{"action":"created","comment":{"url":"https://api.github.com/repos/openshift/origin/pulls/comments/35377517","id":35377517,"diff_hunk":"@@ -0,0 +1,1743 @@\n+package f5\n+\n+import (\n+\t\"bytes\"\n+\t\"crypto/tls\"\n+\t\"encoding/json\"\n+\t\"fmt\"\n+\t\"io\"\n+\t\"io/ioutil\"\n+\t\"net/http\"\n+\t\"os\"\n+\t\"os/exec\"\n+\t\"strings\"\n+\n+\t\"github.com/golang/glog\"\n+)\n+\n+// Error implements the error interface.\n+func (err F5Error) Error() string {\n+\tvar msg string\n+\n+\tif err.err != nil {\n+\t\tmsg = fmt.Sprintf(\"error: %v\", err.err)\n+\t} else if err.Message != nil {\n+\t\tmsg = fmt.Sprintf(\"HTTP code: %d; error from F5: %s\",\n+\t\t\terr.httpStatusCode, *err.Message)\n+\t} else {\n+\t\tmsg = fmt.Sprintf(\"HTTP code: %d.\", err.httpStatusCode)\n+\t}\n+\n+\treturn fmt.Sprintf(\"Encountered an error on %s request to URL %s: %s\",\n+\t\terr.verb, err.url, msg)\n+}\n+\n+// passthroughRoute represents a passthrough route for the F5 router's internal\n+// state.  In the F5 BIG-IP host itself, we must store this information using\n+// two datagroups: one that makes routename to hostname so that we can\n+// reconstruct this state when initializing the router, and one that maps\n+// hostname to poolname for use by the iRule that handles passthrough routes.\n+type passthroughRoute struct {\n+\thostname string\n+\tpoolname string\n+}\n+\n+// f5LTM represents an F5 BIG-IP instance.\n+type f5LTM struct {\n+\t// f5LTMCfg contains the configuration parameters for an F5 BIG-IP instance.\n+\tf5LTMCfg\n+\n+\t// poolMembers maps pool name to set of pool members, where the pool\n+\t// name is a string and the set of members of a pool is represented by\n+\t// a map with value type bool.  A pool member will be identified by\n+\t// a string of the format \"ipaddress:port\".\n+\tpoolMembers map[string]map[string]bool\n+\n+\t// routes maps vserver name to set of routes.\n+\troutes map[string]map[string]bool\n+\n+\t// passthroughRoutes maps routename to passthroughroute{hostname, poolname}.\n+\tpassthroughRoutes map[string]passthroughRoute\n+}\n+\n+// f5LTMCfg holds configuration for connecting to and issueing iControl\n+// requests against an F5 BIG-IP instance.\n+type f5LTMCfg struct {\n+\t// host specifies the hostname or IP address of the F5 BIG-IP host.\n+\thost string\n+\n+\t// username specifies the username with which we should authenticate with the\n+\t// F5 BIG-IP host.\n+\tusername string\n+\n+\t// password specifies the password with which we should authenticate with the\n+\t// F5 BIG-IP host.\n+\tpassword string\n+\n+\t// httpVserver specifies the name of the vserver object for which we should\n+\t// manipulate policy to add and remove HTTP routes.\n+\thttpVserver string\n+\n+\t// httpsVserver specifies the name of the vserver object for which we should\n+\t// manipulate policy to add and remove HTTPS routes.\n+\thttpsVserver string\n+\n+\t// privkey specifies the path to the SSH private key for that we will use to\n+\t// copy certificates to the F5 BIG-IP host.\n+\tprivkey string\n+\n+\t// insecure specifies whether we should perform strict certificate validation\n+\t// for connections to the F5 BIG-IP host.\n+\tinsecure bool\n+}\n+\n+const (\n+\t// policy is the name of the local traffic policy associated with the vservers\n+\t// for insecure (plaintext, HTTP) routes.  We add and delete rules to and from\n+\t// this policy to configure vhost-based routing.\n+\thttpPolicyName = \"openshift_insecure_routes\"\n+\n+\t// https_policy is the name of the local traffic policy associated with the\n+\t// vservers for secure (TLS/SSL, HTTPS) routes.\n+\thttpsPolicyName = \"openshift_secure_routes\"\n+\n+\t// passthroughRoutesDataGroupName is the name of the datagroup that will be used\n+\t// by our iRule for routing SSL-passthrough routes (see below).\n+\tpassthroughRoutesDataGroupName = \"ssl_passthrough_route_dg\"\n+\n+\t// passthroughHostsDataGroupName is the name of the datagroup that will be used\n+\t// by our iRule for routing SSL-passthrough routes (see below).\n+\tpassthroughHostsDataGroupName = \"ssl_passthrough_servername_dg\"\n+\n+\t// sslPassthroughIRuleName is the name assigned to the sslPassthroughIRule\n+\t// iRule.\n+\tsslPassthroughIRuleName = \"openshift_passthrough_irule\"\n+\n+\t// sslPassthroughIRule is an iRule that examines the servername in TLS\n+\t// connections and routes requests to the corresponding pool if one exists.\n+\t//\n+\t// You probably will not read the following TCL code.  However, if you do, you\n+\t// may wonder, \"What's up with this 'if { a - b == abs(a - b) }' nonsense?\n+\t// Why not simply use 'if { a > b }'? WHAT IS THIS NIMWITTERY?!?\" Rest\n+\t// assured, there is *no* nimwittery here! In fact, the explanation for this\n+\t// curiosity is an incompatibility between Google's JSON encoder and F5's JSON\n+\t// decoder: The former produces escape sequences for the characters <, >, and\n+\t// & (specifically, \\u003c, \\u003e, and \\u0026) that confuse the latter.  Thus\n+\t// as long as we are using Google's JSON encoding library and F5's iControl\n+\t// REST API, we must avoid using the <, >, and & characters.\n+\tsslPassthroughIRule = `\n+when CLIENT_ACCEPTED {\n+  TCP::collect\n+}\n+\n+when CLIENT_DATA {\n+  # Byte 0 is the content type.\n+  # Bytes 1-2 are the TLS version.\n+  # Bytes 3-4 are the TLS payload length.\n+  # Bytes 5-$tls_payload_len are the TLS payload.\n+  binary scan [TCP::payload] cSS tls_content_type tls_version tls_payload_len\n+\n+  switch $tls_version {\n+    \"769\" -\n+    \"770\" -\n+    \"771\" {\n+      # Content type of 22 indicates the TLS payload contains a handshake.\n+      if { $tls_content_type == 22 } {\n+        # Byte 5 (the first byte of the handshake) indicates the handshake\n+        # record type, and a value of 1 signifies that the handshake record is\n+        # a ClientHello.\n+        binary scan [TCP::payload] @5c tls_handshake_record_type\n+        if { $tls_handshake_record_type == 1 } {\n+          # Bytes 6-8 are the handshake length (which we ignore).\n+          # Bytes 9-10 are the TLS version (which we ignore).\n+          # Bytes 11-42 are random data (which we ignore).\n+\n+          # Byte 43 is the session ID length.  Following this are three\n+          # variable-length fields which we shall skip over.\n+          set record_offset 43\n+\n+          # Skip the session ID.\n+          binary scan [TCP::payload] @${record_offset}c tls_session_id_len\n+          incr record_offset [expr {1 + $tls_session_id_len}]\n+\n+          # Skip the cipher_suites field.\n+          binary scan [TCP::payload] @${record_offset}S tls_cipher_suites_len\n+          incr record_offset [expr {2 + $tls_cipher_suites_len}]\n+\n+          # Skip the compression_methods field.\n+          binary scan [TCP::payload] @${record_offset}c tls_compression_methods_len\n+          incr record_offset [expr {1 + $tls_compression_methods_len}]\n+\n+          # Get the number of extensions, and store the extensions.\n+          binary scan [TCP::payload] @${record_offset}S tls_extensions_len\n+          incr record_offset 2\n+          binary scan [TCP::payload] @${record_offset}a* tls_extensions\n+\n+          for { set extension_start 0 }\n+              { $tls_extensions_len - $extension_start == abs($tls_extensions_len - $extension_start) }\n+              { incr extension_start 4 } {\n+            # Bytes 0-1 of the extension are the extension type.\n+            # Bytes 2-3 of the extension are the extension length.\n+            binary scan $tls_extensions @${extension_start}SS extension_type extension_len\n+\n+            # Extension type 00 is the ServerName extension.\n+            if { $extension_type == \"00\" } {\n+              # Bytes 4-5 of the extension are the SNI length (we ignore this).\n+\n+              # Byte 6 of the extension is the SNI type.\n+              set sni_type_offset [expr {$extension_start + 6}]\n+              binary scan $tls_extensions @${sni_type_offset}S sni_type\n+\n+              # Type 0 is host_name.\n+              if { $sni_type == \"0\" } {\n+                # Bytes 7-8 of the extension are the SNI data (host_name)\n+                # length.\n+                set sni_len_offset [expr {$extension_start + 7}]\n+                binary scan $tls_extensions @${sni_len_offset}S sni_len\n+\n+                # Bytes 9-$sni_len are the SNI data (host_name).\n+                set sni_start [expr {$extension_start + 9}]\n+                binary scan $tls_extensions @${sni_start}A${sni_len} tls_servername\n+              }\n+            }\n+\n+            incr extension_start $extension_len\n+          }\n+\n+          if { [info exists tls_servername] } {\n+            set servername_lower [string tolower $tls_servername]\n+            if { [class match $servername_lower equals ssl_passthrough_servername_dg] } {\n+              pool [class match -value $servername_lower equals ssl_passthrough_servername_dg]\n+              SSL::disable\n+              HTTP::disable\n+            }\n+          }\n+        }\n+      }\n+    }\n+  }\n+\n+  TCP::release\n+}\n+`\n+)\n+\n+// newF5LTM makes a new f5LTM object.\n+func newF5LTM(cfg f5LTMCfg) (*f5LTM, error) {\n+\tif cfg.insecure == true {\n+\t\tglog.Warning(\"Strict certificate verification is *DISABLED*\")\n+\t}\n+\n+\tif cfg.httpVserver == \"\" {\n+\t\tglog.Warning(\"No vserver was specified for HTTP connections;\" +\n+\t\t\t\" HTTP routes will not be configured\")\n+\t}\n+\n+\tif cfg.httpsVserver == \"\" {\n+\t\tglog.Warning(\"No vserver was specified for HTTPS connections;\" +\n+\t\t\t\" HTTPS routes will not be configured\")\n+\t}\n+\n+\tif cfg.privkey == \"\" {\n+\t\tglog.Info(\"No SSH key provided for the F5 BIG-IP host;\" +\n+\t\t\t\" TLS configuration for applications is disabled\")\n+\t}\n+\n+\trouter := &f5LTM{\n+\t\tf5LTMCfg: f5LTMCfg{\n+\t\t\thost:         cfg.host,\n+\t\t\tusername:     cfg.username,\n+\t\t\tpassword:     cfg.password,\n+\t\t\thttpVserver:  cfg.httpVserver,\n+\t\t\thttpsVserver: cfg.httpsVserver,\n+\t\t\tprivkey:      cfg.privkey,\n+\t\t\tinsecure:     cfg.insecure,\n+\t\t},\n+\t\tpoolMembers: map[string]map[string]bool{},\n+\t\troutes:      map[string]map[string]bool{},\n+\t}\n+\n+\treturn router, nil\n+}\n+\n+//\n+// Helper routines for REST calls.\n+//\n+\n+// One of three things can happen as a result of a REST request to F5\n+// iControl:\n+//\n+// (1) The request succeeds and F5 returns an HTTP 200 response, possibly with\n+//     a result payload, which should have the fields defined in the\n+//     result argument.  In this case, rest_request decodes the payload into\n+//     the result argument and returns nil.\n+//\n+// (2) The request fails and F5 returns an HTTP 4xx or 5xx response with a\n+//     response payload containing a code (which should be the same as the\n+//     HTTP response code) and a string message.  In this case, rest_request\n+//     decodes the response payload and returns an F5Error with the URL, HTTP\n+//     verb, HTTP status code, and error information from the response payload.\n+//\n+// (3) The REST call fails in some other way, such as a socket error or an\n+//     error decoding the result payload.  In this case, rest_request returns\n+//     an F5Error with the URL, HTTP verb, HTTP status code (if any), and error\n+//     value.\n+func (f5 *f5LTM) rest_request(verb string, url string, payload io.Reader,\n+\tresult interface{}) error {\n+\ttr := &http.Transport{\n+\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: f5.insecure},\n+\t}\n+\n+\terrorResult := F5Error{verb: verb, url: url}\n+\n+\treq, err := http.NewRequest(verb, url, payload)\n+\tif err != nil {\n+\t\terrorResult.err = fmt.Errorf(\"http.NewRequest failed: %v\", err)\n+\t\treturn errorResult\n+\t}\n+\n+\treq.Header.Set(\"Content-Type\", \"application/json\")\n+\treq.Header.Set(\"Accept\", \"application/json\")\n+\treq.SetBasicAuth(f5.username, f5.password)\n+\n+\tclient := &http.Client{Transport: tr}\n+\n+\tresp, err := client.Do(req)\n+\tif err != nil {\n+\t\terrorResult.err = fmt.Errorf(\"client.Do failed: %v\", err)\n+\t\treturn errorResult\n+\t}\n+\n+\tdefer resp.Body.Close()\n+\n+\terrorResult.httpStatusCode = resp.StatusCode\n+\n+\tdecoder := json.NewDecoder(resp.Body)\n+\tif resp.StatusCode != 200 {\n+\t\tif resp.StatusCode == 404 {\n+\t\t\t// Just return the error without trying to decode the message\n+\t\t\t// body—F5 sometimes returns an HTML response in the case of a 404\n+\t\t\t// (even though we *ask* for JSON and F5 *usually* obliges).\n+\t\t\treturn errorResult\n+\t\t}\n+\t\terr = decoder.Decode(&errorResult)\n+\t\tif err != nil {\n+\t\t\terrorResult.err = fmt.Errorf(\"Decoder.Decode failed: %v\", err)\n+\t\t}\n+\t\treturn errorResult\n+\t} else if result != nil {\n+\t\terr = decoder.Decode(result)\n+\t\tif err != nil {\n+\t\t\terrorResult.err = fmt.Errorf(\"Decoder.Decode failed: %v\", err)\n+\t\t\treturn errorResult\n+\t\t}\n+\t}\n+\n+\treturn nil\n+}\n+\n+// rest_request_payload is a helper for F5 operations that take\n+// a payload.\n+func (f5 *f5LTM) rest_request_payload(verb string, url string,\n+\tpayload interface{}, result interface{}) error {\n+\tjsonStr, err := json.Marshal(payload)\n+\tif err != nil {\n+\t\treturn F5Error{verb: verb, url: url, err: err}\n+\t}\n+\n+\tencodedPayload := bytes.NewBuffer(jsonStr)\n+\n+\treturn f5.rest_request(verb, url, encodedPayload, result)\n+}\n+\n+// get issues a GET request against the F5 iControl REST API.\n+func (f5 *f5LTM) get(url string, result interface{}) error {\n+\treturn f5.rest_request(\"GET\", url, nil, result)\n+}\n+\n+// post issues a POST request against the F5 iControl REST API.\n+func (f5 *f5LTM) post(url string, payload interface{}, result interface{}) error {\n+\treturn f5.rest_request_payload(\"POST\", url, payload, result)\n+}\n+\n+// patch issues a PATCH request against the F5 iControl REST API.\n+func (f5 *f5LTM) patch(url string, payload interface{}, result interface{}) error {\n+\treturn f5.rest_request_payload(\"PATCH\", url, payload, result)\n+}\n+\n+// delete issues a DELETE request against the F5 iControl REST API.\n+func (f5 *f5LTM) delete(url string, result interface{}) error {\n+\treturn f5.rest_request(\"DELETE\", url, nil, result)\n+}\n+\n+// The documentation for F5 iControl says that F5 uses tildes (~) to substitute\n+// for slashes (/) in URLs, so it should be safe to use slashes in the name of\n+// a rule, for example, but if we do, then we get mysterious errors and rules\n+// randomly appearing and disappearing with messages such as \"err mcpd[4613]:\n+// 01071706:3: Policy '/Common/openshift_insecure_routes', rule\n+// 'foo.cloudapps.example.com/bar'; missing operand.\" in /var/log/ltm and\n+// alternatively appearing with slashes or with tildes in the F5 management\n+// console.  To avoid problems and confusion, we just substitute underscores (_)\n+// for slashes (/) in both JSON and URIs.\n+func f5UrlEscape(s string) string {\n+\treturn strings.Replace(s, \"/\", \"_\", -1)\n+}\n+\n+//\n+// Routines for controlling F5.\n+//\n+\n+func (f5 *f5LTM) ensurePolicyExists(policyName string) error {\n+\tglog.V(4).Infof(\"Checking whether policy %s exists...\", policyName)\n+\n+\tpolicyUrl := fmt.Sprintf(\"https://%s/mgmt/tm/ltm/policy/%s\",\n+\t\tf5.host, policyName)\n+\n+\terr := f5.get(policyUrl, nil)\n+\tif err != nil && err.(F5Error).httpStatusCode != 404 {\n+\t\t// 404 is expected, but anything else really is an error.\n+\t\treturn err\n+\t}\n+\n+\tif err == nil {\n+\t\tglog.V(4).Infof(\"Policy %s already exists; nothing to do.\", policyName)\n+\t\treturn nil\n+\t}\n+\n+\tglog.V(4).Infof(\"Policy %s does not exist; creating it now...\", policyName)\n+\n+\tpoliciesUrl := fmt.Sprintf(\"https://%s/mgmt/tm/ltm/policy\", f5.host)\n+\n+\tpolicyPayload := struct {\n+\t\tName     string   `json:\"name\"`\n+\t\tControls []string `json:\"controls\"`\n+\t\tRequires []string `json:\"requires\"`\n+\t\tStrategy string   `json:\"strategy\"`\n+\t}{\n+\t\tName:     policyName,\n+\t\tControls: []string{\"forwarding\"},\n+\t\tRequires: []string{\"http\"},\n+\t\tStrategy: \"best-match\",\n+\t}\n+\n+\terr = f5.post(policiesUrl, policyPayload, nil)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\t// We need a rule in the policy in order to be able to add the policy to the\n+\t// vservers, so create a no-op rule now.\n+\n+\tglog.V(4).Infof(\"Policy %s created.  Adding no-op rule...\", policyName)\n+\n+\trulesUrl := fmt.Sprintf(\"https://%s/mgmt/tm/ltm/policy/%s/rules\",\n+\t\tf5.host, policyName)\n+\n+\trulesPayload := struct {\n+\t\tName string `json:\"name\"`\n+\t}{\n+\t\tName: \"default_noop\",\n+\t}\n+\n+\terr = f5.post(rulesUrl, rulesPayload, nil)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tglog.V(4).Infof(\"No-op rule added to policy %s.\", policyName)\n+\n+\treturn nil\n+}\n+\n+func (f5 *f5LTM) ensureVserverHasPolicy(vserverName, policyName string) error {\n+\tglog.V(4).Infof(\"Checking whether vserver %s has policy %s...\",\n+\t\tvserverName, policyName)\n+\n+\t// We could use fmt.Sprintf(\"https://%s/mgmt/tm/ltm/virtual/%s/policies/%s\",\n+\t// f5.host, vserverName, policyName) here, except that F5 iControl REST\n+\t// returns a 200 even if the policy does not exist.\n+\tvserverPoliciesUrl := fmt.Sprintf(\"https://%s/mgmt/tm/ltm/virtual/%s/policies\",\n+\t\tf5.host, vserverName)\n+\n+\tres := f5VserverPolicies{}\n+\n+\terr := f5.get(vserverPoliciesUrl, &res)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tfor _, policy := range res.Policies {\n+\t\tif policy.Name == policyName {\n+\t\t\tglog.V(4).Infof(\"Vserver %s has policy %s associated with it;\"+\n+\t\t\t\t\" nothing to do.\", vserverName, policyName)\n+\t\t\treturn nil\n+\t\t}\n+\t}\n+\n+\tglog.V(4).Infof(\"Adding policy %s to vserver %s...\", policyName, vserverName)\n+\n+\tvserverPoliciesPayload := f5VserverPolicy{\n+\t\tName: policyName,\n+\t}\n+\n+\terr = f5.post(vserverPoliciesUrl, vserverPoliciesPayload, nil)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tglog.V(4).Infof(\"Policy %s added to vserver %s.\", policyName, vserverName)\n+\n+\treturn nil\n+}\n+\n+func (f5 *f5LTM) ensureDatagroupExists(datagroupName string) error {\n+\tglog.V(4).Infof(\"Checking whether datagroup %s exists...\", datagroupName)\n+\n+\tdatagroupUrl := fmt.Sprintf(\"https://%s/mgmt/tm/ltm/data-group/internal/%s\",\n+\t\tf5.host, datagroupName)\n+\n+\terr := f5.get(datagroupUrl, nil)\n+\tif err != nil && err.(F5Error).httpStatusCode != 404 {\n+\t\t// 404 is expected, but anything else really is an error.\n+\t\treturn err\n+\t}\n+\n+\tif err == nil {\n+\t\tglog.V(4).Infof(\"Datagroup %s exists; nothing to do.\",\n+\t\t\tdatagroupName)\n+\t\treturn nil\n+\t}\n+\n+\tglog.V(4).Infof(\"Creating datagroup %s...\", datagroupName)\n+\n+\tdatagroupsUrl := fmt.Sprintf(\"https://%s/mgmt/tm/ltm/data-group/internal\",\n+\t\tf5.host)\n+\n+\tdatagroupPayload := struct {\n+\t\tName string `json:\"name\"`\n+\t\tType string `json:\"type\"`\n+\t}{\n+\t\tName: datagroupName,\n+\t\tType: \"string\",\n+\t}\n+\n+\terr = f5.post(datagroupsUrl, datagroupPayload, nil)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tglog.V(4).Infof(\"Datagroup %s created.\", datagroupName)\n+\n+\treturn nil\n+}\n+\n+func (f5 *f5LTM) ensureIRuleExists(iRuleName, iRule string) error {\n+\tglog.V(4).Infof(\"Checking whether iRule %s exists...\", iRuleName)\n+\n+\tiRuleUrl := fmt.Sprintf(\"https://%s/mgmt/tm/ltm/rule/%s\", f5.host, iRuleName)\n+\n+\terr := f5.get(iRuleUrl, nil)\n+\tif err != nil && err.(F5Error).httpStatusCode != 404 {\n+\t\t// 404 is expected, but anything else really is an error.\n+\t\treturn err\n+\t}\n+\n+\tif err == nil {\n+\t\tglog.V(4).Infof(\"iRule %s already exists; nothing to do.\", iRuleName)\n+\t\treturn nil\n+\t}\n+\n+\tglog.V(4).Infof(\"IRule %s does not exist; creating it now...\", iRuleName)\n+\n+\tiRulesUrl := fmt.Sprintf(\"https://%s/mgmt/tm/ltm/rule\", f5.host)\n+\n+\tiRulePayload := struct {\n+\t\tName string `json:\"name\"`\n+\t\tCode string `json:\"apiAnonymous\"`\n+\t}{\n+\t\tName: iRuleName,\n+\t\tCode: iRule,\n+\t}\n+\n+\terr = f5.post(iRulesUrl, iRulePayload, nil)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tglog.V(4).Infof(\"IRule %s created.\", iRuleName)\n+\n+\treturn nil\n+}\n+\n+func (f5 *f5LTM) ensureVserverHasIRule(vserverName, iRuleName string) error {\n+\tglog.V(4).Infof(\"Checking whether vserver %s has iRule %s...\",\n+\t\tvserverName, iRuleName)\n+\n+\tvserverUrl := fmt.Sprintf(\"https://%s/mgmt/tm/ltm/virtual/%s\",\n+\t\tf5.host, vserverName)\n+\n+\tres := struct {\n+\t\tRules []string `json:\"rules\"`\n+\t}{}\n+\n+\terr := f5.get(vserverUrl, &res)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tcommonIRuleName := fmt.Sprintf(\"/Common/%s\", iRuleName)\n+\n+\tfor _, name := range res.Rules {\n+\t\tif name == commonIRuleName {\n+\t\t\tglog.V(4).Infof(\"Vserver %s has iRule %s associated with it;\"+\n+\t\t\t\t\" nothing to do.\",\n+\t\t\t\tvserverName, iRuleName)\n+\t\t\treturn nil\n+\t\t}\n+\t}\n+\n+\tglog.V(4).Infof(\"Adding iRule %s to vserver %s...\", iRuleName, vserverName)\n+\n+\tvserverRulesPayload := struct {\n+\t\tRules []string `json:\"rules\"`\n+\t}{\n+\t\tRules: []string{sslPassthroughIRuleName},\n+\t}\n+\n+\terr = f5.patch(vserverUrl, vserverRulesPayload, nil)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tglog.V(4).Infof(\"IRule %s added to vserver %s.\", iRuleName, vserverName)\n+\n+\treturn nil\n+}\n+\n+// Initialize ensures that OpenShift-specific configuration is in place\n+// on the F5 BIG-IP host.  In particular, this method creates a traffic\n+// policy and associates it with the vservers if necessary.\n+func (f5 *f5LTM) Initialize() error {\n+\terr := f5.ensurePolicyExists(httpPolicyName)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tif f5.httpVserver != \"\" {\n+\t\terr = f5.ensureVserverHasPolicy(f5.httpVserver, httpPolicyName)\n+\t\tif err != nil {\n+\t\t\treturn err\n+\t\t}\n+\t}\n+\n+\tif f5.httpsVserver != \"\" {\n+\t\terr = f5.ensurePolicyExists(httpsPolicyName)\n+\t\tif err != nil {\n+\t\t\treturn err\n+\t\t}\n+\n+\t\terr = f5.ensureVserverHasPolicy(f5.httpsVserver, httpsPolicyName)\n+\t\tif err != nil {\n+\t\t\treturn err\n+\t\t}\n+\n+\t\tif f5.httpVserver != \"\" {\n+\t\t\terr = f5.ensureVserverHasPolicy(f5.httpVserver, httpPolicyName)\n+\t\t\tif err != nil {\n+\t\t\t\treturn err\n+\t\t\t}\n+\t\t}\n+\n+\t\terr = f5.ensureDatagroupExists(passthroughRoutesDataGroupName)\n+\t\tif err != nil {\n+\t\t\treturn err\n+\t\t}\n+\n+\t\terr = f5.ensureDatagroupExists(passthroughHostsDataGroupName)\n+\t\tif err != nil {\n+\t\t\treturn err\n+\t\t}\n+\n+\t\terr = f5.ensureIRuleExists(sslPassthroughIRuleName, sslPassthroughIRule)\n+\t\tif err != nil {\n+\t\t\treturn err\n+\t\t}\n+\n+\t\terr = f5.ensureVserverHasIRule(f5.httpsVserver, sslPassthroughIRuleName)\n+\t\tif err != nil {\n+\t\t\treturn err\n+\t\t}\n+\t}\n+\n+\tglog.V(4).Infof(\"F5 initialization is complete.\")\n+\n+\treturn nil\n+}\n+\n+// Create a pool named poolname on F5.\n+func (f5 *f5LTM) CreatePool(poolname string) error {\n+\turl := fmt.Sprintf(\"https://%s/mgmt/tm/ltm/pool\", f5.host)\n+\n+\tpayload := struct {\n+\t\tMode    string `json:\"loadBalancingMode\"`\n+\t\tMonitor string `json:\"monitor\"`\n+\t\tName    string `json:\"name\"`\n+\t}{\n+\t\tMode:    \"round-robin\",\n+\t\tMonitor: \"/Common/http\",\n+\t\tName:    poolname,\n+\t}\n+\n+\terr := f5.post(url, payload, nil)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\t// We don't really need to initialise f5.poolMembers[poolname] because\n+\t// we always check whether it is initialised before using it, but\n+\t// initialising it to an empty map here saves a REST call later the first\n+\t// time f5.PoolHasMember is invoked with poolname.\n+\tf5.poolMembers[poolname] = map[string]bool{}\n+\n+\tglog.V(4).Infof(\"Pool %s created.\", poolname)\n+\n+\treturn nil\n+}\n+\n+// Delete the pool named poolname from F5, and delete\n+// f5.poolMembers[poolname].\n+func (f5 *f5LTM) DeletePool(poolname string) error {\n+\turl := fmt.Sprintf(\"https://%s/mgmt/tm/ltm/pool/%s\", f5.host, poolname)\n+\n+\terr := f5.delete(url, nil)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\t// Note: We *must* use delete here rather than merely assigning false because\n+\t// len() includes false items, and we want len() to return an accurate count\n+\t// of members.  Also, we probably save some memory by using delete.\n+\tdelete(f5.poolMembers, poolname)\n+\n+\tglog.V(4).Infof(\"Pool %s deleted.\", poolname)\n+\n+\treturn nil\n+}\n+\n+// Return f5.poolMembers[poolname], first initializing it from F5 if it is zero.\n+func (f5 *f5LTM) GetPoolMembers(poolname string) (map[string]bool, error) {\n+\tmembers, ok := f5.poolMembers[poolname]\n+\tif ok {\n+\t\treturn members, nil\n+\t}\n+\n+\turl := fmt.Sprintf(\"https://%s/mgmt/tm/ltm/pool/%s/members\",\n+\t\tf5.host, poolname)\n+\n+\tres := f5Pool{}\n+\n+\terr := f5.get(url, &res)\n+\tif err != nil {\n+\t\treturn nil, err\n+\t}\n+\n+\t// Note that we do not initialise f5.poolMembers[poolname] unless we know that\n+\t// the pool exists (i.e., the above GET request for the pool succeeds).\n+\tf5.poolMembers[poolname] = map[string]bool{}\n+\n+\tfor _, member := range res.Members {\n+\t\tf5.poolMembers[poolname][member.Name] = true\n+\t}\n+\n+\treturn f5.poolMembers[poolname], nil\n+}\n+\n+// Check whether the specified pool exists.  Internally, this method uses\n+// f5.poolMembers[poolname], as a side effect initialising it if it is zero.\n+func (f5 *f5LTM) PoolExists(poolname string) (bool, error) {\n+\t_, err := f5.GetPoolMembers(poolname)\n+\tif err == nil {\n+\t\treturn true, nil\n+\t}\n+\n+\tif err.(F5Error).httpStatusCode == 404 {\n+\t\treturn false, nil\n+\t}\n+\n+\treturn false, err\n+}\n+\n+// Check whether the given member is in the pool named poolname on F5.\n+// Internally, this method uses f5.poolMembers[poolname], initialising\n+// it first if it is zero.\n+func (f5 *f5LTM) PoolHasMember(poolname, member string) (bool, error) {\n+\tmembers, err := f5.GetPoolMembers(poolname)\n+\tif err != nil {\n+\t\treturn false, err\n+\t}\n+\n+\treturn members[member], nil\n+}\n+\n+// Add member to the pool named poolname on F5, and update\n+// f5.poolMembers[poolname].\n+func (f5 *f5LTM) AddPoolMember(poolname, member string) error {\n+\thasMember, err := f5.PoolHasMember(poolname, member)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\tif hasMember {\n+\t\tglog.V(4).Infof(\"Pool %s already has member %s.\\n\", poolname, member)\n+\t\treturn nil\n+\t}\n+\n+\tglog.V(4).Infof(\"Adding pool member %s to pool %s.\", member, poolname)\n+\n+\turl := fmt.Sprintf(\"https://%s/mgmt/tm/ltm/pool/%s/members\",\n+\t\tf5.host, poolname)\n+\n+\tpayload := struct {\n+\t\tMember string `json:\"name\"`\n+\t}{\n+\t\tMember: member,\n+\t}\n+\n+\terr = f5.post(url, payload, nil)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tmembers, err := f5.GetPoolMembers(poolname)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\n+\tmembers[member] = true","path":"plugins/router/f5/f5.go","position":815,"original_position":815,"commit_id":"db4e16ed3ec40624b0dc71d9739b59ad87ee3311","original_commit_id":"68bd4ee29bf73e9496425d8de0272f32afc947d3","user":{"login":"ramr","id":928868,"avatar_url":"https://avatars.githubusercontent.com/u/928868?v=3","gravatar_id":"","url":"https://api.github.com/users/ramr","html_url":"https://github.com/ramr","followers_url":"https://api.github.com/users/ramr/followers","following_url":"https://api.github.com/users/ramr/following{/other_user}","gists_url":"https://api.github.com/users/ramr/gists{/gist_id}","starred_url":"https://api.github.com/users/ramr/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ramr/subscriptions","organizations_url":"https://api.github.com/users/ramr/orgs","repos_url":"https://api.github.com/users/ramr/repos","events_url":"https://api.github.com/users/ramr/events{/privacy}","received_events_url":"https://api.github.com/users/ramr/received_events","type":"User","site_admin":false},"body":"Yeah got that after - as always am a bit confused!! ;^)","created_at":"2015-07-23T22:07:14Z","updated_at":"2015-07-23T22:07:14Z","html_url":"https://github.com/openshift/origin/pull/2940#discussion_r35377517","pull_request_url":"https://api.github.com/repos/openshift/origin/pulls/2940","_links":{"self":{"href":"https://api.github.com/repos/openshift/origin/pulls/comments/35377517"},"html":{"href":"https://github.com/openshift/origin/pull/2940#discussion_r35377517"},"pull_request":{"href":"https://api.github.com/repos/openshift/origin/pulls/2940"}}},"pull_request":{"url":"https://api.github.com/repos/openshift/origin/pulls/2940","id":37166304,"html_url":"https://github.com/openshift/origin/pull/2940","diff_url":"https://github.com/openshift/origin/pull/2940.diff","patch_url":"https://github.com/openshift/origin/pull/2940.patch","issue_url":"https://api.github.com/repos/openshift/origin/issues/2940","number":2940,"state":"open","locked":false,"title":"F5 router plugin","user":{"login":"Miciah","id":2042465,"avatar_url":"https://avatars.githubusercontent.com/u/2042465?v=3","gravatar_id":"","url":"https://api.github.com/users/Miciah","html_url":"https://github.com/Miciah","followers_url":"https://api.github.com/users/Miciah/followers","following_url":"https://api.github.com/users/Miciah/following{/other_user}","gists_url":"https://api.github.com/users/Miciah/gists{/gist_id}","starred_url":"https://api.github.com/users/Miciah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Miciah/subscriptions","organizations_url":"https://api.github.com/users/Miciah/orgs","repos_url":"https://api.github.com/users/Miciah/repos","events_url":"https://api.github.com/users/Miciah/events{/privacy}","received_events_url":"https://api.github.com/users/Miciah/received_events","type":"User","site_admin":false},"body":"","created_at":"2015-06-08T15:38:13Z","updated_at":"2015-07-23T22:07:14Z","closed_at":null,"merged_at":null,"merge_commit_sha":"bc1bb3f7c12c62e698c5294d9b05b308a23e50bc","assignee":null,"milestone":{"url":"https://api.github.com/repos/openshift/origin/milestones/5","html_url":"https://github.com/openshift/origin/milestones/1.1.0","labels_url":"https://api.github.com/repos/openshift/origin/milestones/5/labels","id":1119756,"number":5,"title":"1.1.0","description":"Corresponds to the 3.1.0 OSE release","creator":{"login":"smarterclayton","id":1163175,"avatar_url":"https://avatars.githubusercontent.com/u/1163175?v=3","gravatar_id":"","url":"https://api.github.com/users/smarterclayton","html_url":"https://github.com/smarterclayton","followers_url":"https://api.github.com/users/smarterclayton/followers","following_url":"https://api.github.com/users/smarterclayton/following{/other_user}","gists_url":"https://api.github.com/users/smarterclayton/gists{/gist_id}","starred_url":"https://api.github.com/users/smarterclayton/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/smarterclayton/subscriptions","organizations_url":"https://api.github.com/users/smarterclayton/orgs","repos_url":"https://api.github.com/users/smarterclayton/repos","events_url":"https://api.github.com/users/smarterclayton/events{/privacy}","received_events_url":"https://api.github.com/users/smarterclayton/received_events","type":"User","site_admin":false},"open_issues":223,"closed_issues":93,"state":"open","created_at":"2015-05-20T04:04:56Z","updated_at":"2015-07-23T19:15:05Z","due_on":"2015-08-29T04:00:00Z","closed_at":null},"commits_url":"https://api.github.com/repos/openshift/origin/pulls/2940/commits","review_comments_url":"https://api.github.com/repos/openshift/origin/pulls/2940/comments","review_comment_url":"https://api.github.com/repos/openshift/origin/pulls/comments{/number}","comments_url":"https://api.github.com/repos/openshift/origin/issues/2940/comments","statuses_url":"https://api.github.com/repos/openshift/origin/statuses/db4e16ed3ec40624b0dc71d9739b59ad87ee3311","head":{"label":"Miciah:f5-router-plugin","ref":"f5-router-plugin","sha":"db4e16ed3ec40624b0dc71d9739b59ad87ee3311","user":{"login":"Miciah","id":2042465,"avatar_url":"https://avatars.githubusercontent.com/u/2042465?v=3","gravatar_id":"","url":"https://api.github.com/users/Miciah","html_url":"https://github.com/Miciah","followers_url":"https://api.github.com/users/Miciah/followers","following_url":"https://api.github.com/users/Miciah/following{/other_user}","gists_url":"https://api.github.com/users/Miciah/gists{/gist_id}","starred_url":"https://api.github.com/users/Miciah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Miciah/subscriptions","organizations_url":"https://api.github.com/users/Miciah/orgs","repos_url":"https://api.github.com/users/Miciah/repos","events_url":"https://api.github.com/users/Miciah/events{/privacy}","received_events_url":"https://api.github.com/users/Miciah/received_events","type":"User","site_admin":false},"repo":{"id":31791406,"name":"origin","full_name":"Miciah/origin","owner":{"login":"Miciah","id":2042465,"avatar_url":"https://avatars.githubusercontent.com/u/2042465?v=3","gravatar_id":"","url":"https://api.github.com/users/Miciah","html_url":"https://github.com/Miciah","followers_url":"https://api.github.com/users/Miciah/followers","following_url":"https://api.github.com/users/Miciah/following{/other_user}","gists_url":"https://api.github.com/users/Miciah/gists{/gist_id}","starred_url":"https://api.github.com/users/Miciah/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Miciah/subscriptions","organizations_url":"https://api.github.com/users/Miciah/orgs","repos_url":"https://api.github.com/users/Miciah/repos","events_url":"https://api.github.com/users/Miciah/events{/privacy}","received_events_url":"https://api.github.com/users/Miciah/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/Miciah/origin","description":"OpenShift 3 - build, deploy, and manage your applications with Docker and Kubernetes","fork":true,"url":"https://api.github.com/repos/Miciah/origin","forks_url":"https://api.github.com/repos/Miciah/origin/forks","keys_url":"https://api.github.com/repos/Miciah/origin/keys{/key_id}","collaborators_url":"https://api.github.com/repos/Miciah/origin/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/Miciah/origin/teams","hooks_url":"https://api.github.com/repos/Miciah/origin/hooks","issue_events_url":"https://api.github.com/repos/Miciah/origin/issues/events{/number}","events_url":"https://api.github.com/repos/Miciah/origin/events","assignees_url":"https://api.github.com/repos/Miciah/origin/assignees{/user}","branches_url":"https://api.github.com/repos/Miciah/origin/branches{/branch}","tags_url":"https://api.github.com/repos/Miciah/origin/tags","blobs_url":"https://api.github.com/repos/Miciah/origin/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/Miciah/origin/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/Miciah/origin/git/refs{/sha}","trees_url":"https://api.github.com/repos/Miciah/origin/git/trees{/sha}","statuses_url":"https://api.github.com/repos/Miciah/origin/statuses/{sha}","languages_url":"https://api.github.com/repos/Miciah/origin/languages","stargazers_url":"https://api.github.com/repos/Miciah/origin/stargazers","contributors_url":"https://api.github.com/repos/Miciah/origin/contributors","subscribers_url":"https://api.github.com/repos/Miciah/origin/subscribers","subscription_url":"https://api.github.com/repos/Miciah/origin/subscription","commits_url":"https://api.github.com/repos/Miciah/origin/commits{/sha}","git_commits_url":"https://api.github.com/repos/Miciah/origin/git/commits{/sha}","comments_url":"https://api.github.com/repos/Miciah/origin/comments{/number}","issue_comment_url":"https://api.github.com/repos/Miciah/origin/issues/comments{/number}","contents_url":"https://api.github.com/repos/Miciah/origin/contents/{+path}","compare_url":"https://api.github.com/repos/Miciah/origin/compare/{base}...{head}","merges_url":"https://api.github.com/repos/Miciah/origin/merges","archive_url":"https://api.github.com/repos/Miciah/origin/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/Miciah/origin/downloads","issues_url":"https://api.github.com/repos/Miciah/origin/issues{/number}","pulls_url":"https://api.github.com/repos/Miciah/origin/pulls{/number}","milestones_url":"https://api.github.com/repos/Miciah/origin/milestones{/number}","notifications_url":"https://api.github.com/repos/Miciah/origin/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/Miciah/origin/labels{/name}","releases_url":"https://api.github.com/repos/Miciah/origin/releases{/id}","created_at":"2015-03-06T22:14:05Z","updated_at":"2015-03-06T22:14:07Z","pushed_at":"2015-07-22T16:46:31Z","git_url":"git://github.com/Miciah/origin.git","ssh_url":"git@github.com:Miciah/origin.git","clone_url":"https://github.com/Miciah/origin.git","svn_url":"https://github.com/Miciah/origin","homepage":"http://www.openshift.org","size":115273,"stargazers_count":0,"watchers_count":0,"language":"Go","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"openshift:master","ref":"master","sha":"f3dc8995bfd4028c89978d0b04bbe45ff2564f45","user":{"login":"openshift","id":792337,"avatar_url":"https://avatars.githubusercontent.com/u/792337?v=3","gravatar_id":"","url":"https://api.github.com/users/openshift","html_url":"https://github.com/openshift","followers_url":"https://api.github.com/users/openshift/followers","following_url":"https://api.github.com/users/openshift/following{/other_user}","gists_url":"https://api.github.com/users/openshift/gists{/gist_id}","starred_url":"https://api.github.com/users/openshift/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/openshift/subscriptions","organizations_url":"https://api.github.com/users/openshift/orgs","repos_url":"https://api.github.com/users/openshift/repos","events_url":"https://api.github.com/users/openshift/events{/privacy}","received_events_url":"https://api.github.com/users/openshift/received_events","type":"Organization","site_admin":false},"repo":{"id":22442668,"name":"origin","full_name":"openshift/origin","owner":{"login":"openshift","id":792337,"avatar_url":"https://avatars.githubusercontent.com/u/792337?v=3","gravatar_id":"","url":"https://api.github.com/users/openshift","html_url":"https://github.com/openshift","followers_url":"https://api.github.com/users/openshift/followers","following_url":"https://api.github.com/users/openshift/following{/other_user}","gists_url":"https://api.github.com/users/openshift/gists{/gist_id}","starred_url":"https://api.github.com/users/openshift/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/openshift/subscriptions","organizations_url":"https://api.github.com/users/openshift/orgs","repos_url":"https://api.github.com/users/openshift/repos","events_url":"https://api.github.com/users/openshift/events{/privacy}","received_events_url":"https://api.github.com/users/openshift/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/openshift/origin","description":"OpenShift 3 - build, deploy, and manage your applications with Docker and Kubernetes","fork":false,"url":"https://api.github.com/repos/openshift/origin","forks_url":"https://api.github.com/repos/openshift/origin/forks","keys_url":"https://api.github.com/repos/openshift/origin/keys{/key_id}","collaborators_url":"https://api.github.com/repos/openshift/origin/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/openshift/origin/teams","hooks_url":"https://api.github.com/repos/openshift/origin/hooks","issue_events_url":"https://api.github.com/repos/openshift/origin/issues/events{/number}","events_url":"https://api.github.com/repos/openshift/origin/events","assignees_url":"https://api.github.com/repos/openshift/origin/assignees{/user}","branches_url":"https://api.github.com/repos/openshift/origin/branches{/branch}","tags_url":"https://api.github.com/repos/openshift/origin/tags","blobs_url":"https://api.github.com/repos/openshift/origin/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/openshift/origin/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/openshift/origin/git/refs{/sha}","trees_url":"https://api.github.com/repos/openshift/origin/git/trees{/sha}","statuses_url":"https://api.github.com/repos/openshift/origin/statuses/{sha}","languages_url":"https://api.github.com/repos/openshift/origin/languages","stargazers_url":"https://api.github.com/repos/openshift/origin/stargazers","contributors_url":"https://api.github.com/repos/openshift/origin/contributors","subscribers_url":"https://api.github.com/repos/openshift/origin/subscribers","subscription_url":"https://api.github.com/repos/openshift/origin/subscription","commits_url":"https://api.github.com/repos/openshift/origin/commits{/sha}","git_commits_url":"https://api.github.com/repos/openshift/origin/git/commits{/sha}","comments_url":"https://api.github.com/repos/openshift/origin/comments{/number}","issue_comment_url":"https://api.github.com/repos/openshift/origin/issues/comments{/number}","contents_url":"https://api.github.com/repos/openshift/origin/contents/{+path}","compare_url":"https://api.github.com/repos/openshift/origin/compare/{base}...{head}","merges_url":"https://api.github.com/repos/openshift/origin/merges","archive_url":"https://api.github.com/repos/openshift/origin/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/openshift/origin/downloads","issues_url":"https://api.github.com/repos/openshift/origin/issues{/number}","pulls_url":"https://api.github.com/repos/openshift/origin/pulls{/number}","milestones_url":"https://api.github.com/repos/openshift/origin/milestones{/number}","notifications_url":"https://api.github.com/repos/openshift/origin/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/openshift/origin/labels{/name}","releases_url":"https://api.github.com/repos/openshift/origin/releases{/id}","created_at":"2014-07-30T20:04:05Z","updated_at":"2015-07-23T12:15:00Z","pushed_at":"2015-07-23T22:04:33Z","git_url":"git://github.com/openshift/origin.git","ssh_url":"git@github.com:openshift/origin.git","clone_url":"https://github.com/openshift/origin.git","svn_url":"https://github.com/openshift/origin","homepage":"http://www.openshift.org","size":195696,"stargazers_count":567,"watchers_count":567,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":316,"mirror_url":null,"open_issues_count":444,"forks":316,"open_issues":444,"watchers":567,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/openshift/origin/pulls/2940"},"html":{"href":"https://github.com/openshift/origin/pull/2940"},"issue":{"href":"https://api.github.com/repos/openshift/origin/issues/2940"},"comments":{"href":"https://api.github.com/repos/openshift/origin/issues/2940/comments"},"review_comments":{"href":"https://api.github.com/repos/openshift/origin/pulls/2940/comments"},"review_comment":{"href":"https://api.github.com/repos/openshift/origin/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/openshift/origin/pulls/2940/commits"},"statuses":{"href":"https://api.github.com/repos/openshift/origin/statuses/db4e16ed3ec40624b0dc71d9739b59ad87ee3311"}}}}