{"action":"created","comment":{"url":"https://api.github.com/repos/openshift/origin/pulls/comments/171705637","pull_request_review_id":100607316,"id":171705637,"diff_hunk":"@@ -0,0 +1,591 @@\n+package ingress\n+\n+import (\n+\t\"fmt\"\n+\t\"math\"\n+\t\"time\"\n+\n+\t\"github.com/golang/glog\"\n+\n+\t\"k8s.io/api/core/v1\"\n+\textensionsv1beta1 \"k8s.io/api/extensions/v1beta1\"\n+\t\"k8s.io/apimachinery/pkg/api/errors\"\n+\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n+\t\"k8s.io/apimachinery/pkg/labels\"\n+\t\"k8s.io/apimachinery/pkg/types\"\n+\t\"k8s.io/apimachinery/pkg/util/intstr\"\n+\t\"k8s.io/apimachinery/pkg/util/json\"\n+\tutilruntime \"k8s.io/apimachinery/pkg/util/runtime\"\n+\t\"k8s.io/apimachinery/pkg/util/wait\"\n+\tcoreinformers \"k8s.io/client-go/informers/core/v1\"\n+\textensionsinformers \"k8s.io/client-go/informers/extensions/v1beta1\"\n+\tkv1core \"k8s.io/client-go/kubernetes/typed/core/v1\"\n+\tcorelisters \"k8s.io/client-go/listers/core/v1\"\n+\textensionslisters \"k8s.io/client-go/listers/extensions/v1beta1\"\n+\t\"k8s.io/client-go/tools/cache\"\n+\t\"k8s.io/client-go/tools/record\"\n+\t\"k8s.io/client-go/util/workqueue\"\n+\t\"k8s.io/kubernetes/pkg/api/legacyscheme\"\n+\n+\troutev1 \"github.com/openshift/api/route/v1\"\n+\trouteclient \"github.com/openshift/client-go/route/clientset/versioned/typed/route/v1\"\n+\trouteinformers \"github.com/openshift/client-go/route/informers/externalversions/route/v1\"\n+\troutelisters \"github.com/openshift/client-go/route/listers/route/v1\"\n+)\n+\n+const (\n+\t// maxRetries is the number of times an ingress will be retried before it is dropped out of the queue.\n+\tmaxRetries          = 5\n+\tmaxResourceInterval = 30 * time.Second\n+)\n+\n+// defaultResourceFailureDelay will retry failures forever, but implements an exponential\n+// capped backoff after a certain limit.\n+func defaultResourceFailureDelay(requeue int) (time.Duration, bool) {\n+\tif requeue > 5 {\n+\t\treturn maxResourceInterval, true\n+\t}\n+\tt := time.Duration(math.Pow(2.0, float64(requeue)) * float64(time.Second))\n+\tif t > maxResourceInterval {\n+\t\tt = maxResourceInterval\n+\t}\n+\treturn t, true\n+}\n+\n+// Controller ensures that zero or more routes exist to match any supported ingress. The\n+// controller creates a controller owner reference from the route to the parent ingress,\n+// allowing users to orphan their ingress. All owned routes have specific spec fields\n+// managed (those attributes present on the ingress), while any other fields may be\n+// modified by the user.\n+//\n+// Invariants:\n+//\n+// 1. For every ingress path rule with a non-empty backend statement, a route should\n+//    exist that points to that backend.\n+// 2. For every TLS hostname that has a corresponding path rule and points to a secret\n+//    that exists, a route should exist with a valid TLS config from that secret.\n+// 3. For every service referenced by the ingress path rule, the route should have\n+//    an update to date target port based on the service.\n+// 4. A route owned by an ingress that no longer satisfies the first three invariants\n+//    should be deleted.\n+//\n+// Unsupported attributes:\n+//\n+// * the ingress class attribute\n+// * nginx annotations\n+// * the empty backend\n+// * paths with empty backends\n+// * creating a dynamic route spec.host\n+//\n+type Controller struct {\n+\teventRecorder record.EventRecorder\n+\n+\t// Allows injection for testing, controls requeues on errors\n+\tresourceFailureDelayFn func(requeue int) (time.Duration, bool)\n+\n+\tclient        routeclient.RoutesGetter\n+\tsecretLister  corelisters.SecretLister\n+\tingressLister extensionslisters.IngressLister\n+\trouteLister   routelisters.RouteLister\n+\tserviceLister corelisters.ServiceLister\n+\n+\t// queue is the list of namespace keys that must be synced.\n+\tqueue workqueue.RateLimitingInterface\n+\n+\t// syncs are the items that must return true before the queue can be processed\n+\tsyncs []cache.InformerSynced\n+}\n+\n+type queueKey struct {\n+\tnamespace string\n+\tname      string\n+}\n+\n+// NewController instantiates a Controller\n+func NewController(eventsClient kv1core.EventsGetter, client routeclient.RoutesGetter, ingresses extensionsinformers.IngressInformer, secrets coreinformers.SecretInformer, services coreinformers.ServiceInformer, routes routeinformers.RouteInformer) *Controller {\n+\tbroadcaster := record.NewBroadcaster()\n+\tbroadcaster.StartLogging(glog.Infof)\n+\t// TODO: remove the wrapper when every clients have moved to use the clientset.\n+\tbroadcaster.StartRecordingToSink(&kv1core.EventSinkImpl{Interface: eventsClient.Events(\"\")})\n+\trecorder := broadcaster.NewRecorder(legacyscheme.Scheme, v1.EventSource{Component: \"ingress-to-route-controller\"})\n+\n+\tc := &Controller{\n+\t\teventRecorder: recorder,\n+\t\tqueue:         workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), \"ingress-to-route\"),\n+\n+\t\tclient:        client,\n+\t\tingressLister: ingresses.Lister(),\n+\t\tsecretLister:  secrets.Lister(),\n+\t\trouteLister:   routes.Lister(),\n+\t\tserviceLister: services.Lister(),\n+\n+\t\tresourceFailureDelayFn: defaultResourceFailureDelay,\n+\t}\n+\n+\tprocessNamespaceFn := func(obj interface{}) {\n+\t\tswitch t := obj.(type) {\n+\t\tcase metav1.Object:\n+\t\t\tns := t.GetNamespace()\n+\t\t\tif len(ns) == 0 {\n+\t\t\t\tutilruntime.HandleError(fmt.Errorf(\"object %T has no namespace\", obj))\n+\t\t\t\treturn\n+\t\t\t}\n+\t\t\tc.queue.Add(queueKey{namespace: ns})\n+\t\tdefault:\n+\t\t\tutilruntime.HandleError(fmt.Errorf(\"couldn't get key for object %T\", obj))\n+\t\t}\n+\t}\n+\n+\t// any change to a secret of type TLS in the namespace\n+\tsecrets.Informer().AddEventHandler(cache.FilteringResourceEventHandler{\n+\t\tFilterFunc: func(obj interface{}) bool {\n+\t\t\tswitch t := obj.(type) {\n+\t\t\tcase *v1.Secret:\n+\t\t\t\treturn t.Type == v1.SecretTypeTLS\n+\t\t\t}\n+\t\t\treturn true\n+\t\t},\n+\t\tHandler: cache.ResourceEventHandlerFuncs{\n+\t\t\tAddFunc:    processNamespaceFn,\n+\t\t\tDeleteFunc: processNamespaceFn,","path":"pkg/route/controller/ingress/ingress.go","position":150,"original_position":150,"commit_id":"dd0be4c99eb51e8018ed594151fac7f93fc112e1","original_commit_id":"dd0be4c99eb51e8018ed594151fac7f93fc112e1","user":{"login":"smarterclayton","id":1163175,"avatar_url":"https://avatars0.githubusercontent.com/u/1163175?v=4","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},"body":"we queue a tombstone by assuming that anything not a secret is probably a tombstone.  It didn't feel like handling it specially added anything over \"if we don't recognize it, queue anyway\"","created_at":"2018-03-01T21:53:05Z","updated_at":"2018-03-01T21:53:05Z","html_url":"https://github.com/openshift/origin/pull/18658#discussion_r171705637","pull_request_url":"https://api.github.com/repos/openshift/origin/pulls/18658","author_association":"OWNER","_links":{"self":{"href":"https://api.github.com/repos/openshift/origin/pulls/comments/171705637"},"html":{"href":"https://github.com/openshift/origin/pull/18658#discussion_r171705637"},"pull_request":{"href":"https://api.github.com/repos/openshift/origin/pulls/18658"}},"in_reply_to_id":171649611},"pull_request":{"url":"https://api.github.com/repos/openshift/origin/pulls/18658","id":169840667,"html_url":"https://github.com/openshift/origin/pull/18658","diff_url":"https://github.com/openshift/origin/pull/18658.diff","patch_url":"https://github.com/openshift/origin/pull/18658.patch","issue_url":"https://api.github.com/repos/openshift/origin/issues/18658","number":18658,"state":"open","locked":false,"title":"Replace router support for ingress with an ingress-to-route controller","user":{"login":"smarterclayton","id":1163175,"avatar_url":"https://avatars0.githubusercontent.com/u/1163175?v=4","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},"body":"The existing ingress support in the router has a number of serious problems:\r\n\r\n1. We cannot give a router \"watch all secrets safely\", so secret updates are delayed to the full resync interval\r\n2. Granting a router access to individual secrets requires some sort of ingress aware controller\r\n3. There are race conditions between secret update where a secret may not change for a full resync interval\r\n4. Users get no status on ingress, and the UI cannot show anything about them, which means cert and overlap failures are opaque\r\n\r\nThis PR changes from the routers handling ingress to a new `ingress-to-route` controller that creates actual routes for each ingress, then keeps them in sync. It also has permission to watch secrets and will update the routes when those change, which means routers observe ingress secret changes as quickly as possible.  Each child route has an owner/controller ref back to the ingress, which means garbage collection manages its lifecycle.\r\n\r\nExample:\r\n\r\n```\r\nkind: Ingress\r\napiVersion: extensions/v1beta1\r\nmetadata:\r\n  name: test\r\nspec:\r\n  rules:\r\n  - host: test.com\r\n     http:\r\n       paths:\r\n       - path: /test \r\n         backend:\r\n           serviceName: test-1\r\n           servicePort: 80\r\n\r\n-->\r\n\r\nkind: Route\r\napiVersion: route.openshift.io/v1\r\nmetadata:\r\n  name: test-a34th\r\n  ownerReferences:\r\n  - apiVersion: extensions/v1beta1\r\n     kind: Ingress\r\n     name: test\r\n     controller: true\r\nspec:\r\n  host: test.com\r\n  path: /test\r\n  to:\r\n    name: test-1\r\n  port:\r\n     targetPort: 80\r\n```\r\n\r\nIt is also much cleaner, easier to test (unit and in isolation), easier to review, and simplifies router code significantly. I had originally looked at implementing this as an RBAC sync controller (granting routers access to secrets) but that only exposed the real problem which was that routers can't watch on secrets. Since Routes are generally better designed as a secure API than ingress, and ingress is likely to substantially change over the next few releases, this approach allows us to continue to shield our users from the impact of that uncertainty while preserving all the good things about routes.\r\n\r\nSupporting ingress correctly is a key short term goal so that we can adapt users in the ecosystem who depend on ingress.\r\n\r\nThis adds no feature support above for ingress above and beyond, but it is relatively easy for us to adapt community annotations that can use our features.\r\n\r\nStill remaining:\r\n\r\n* [x] Add an e2e test that verifies the router serves a materialized ingress\r\n* [ ] Determine how/whether to default spec.host (arguably we shouldn't be supporting this since ingress controllers don't have this concept)\r\n* [ ] Decide whether to expose load balancer status","created_at":"2018-02-18T17:32:36Z","updated_at":"2018-03-01T21:53:05Z","closed_at":null,"merged_at":null,"merge_commit_sha":"917498c1ac272f0740eec45b1e8478191b1d063c","assignee":null,"assignees":[],"requested_reviewers":[{"login":"kargakis","id":3900799,"avatar_url":"https://avatars3.githubusercontent.com/u/3900799?v=4","gravatar_id":"","url":"https://api.github.com/users/kargakis","html_url":"https://github.com/kargakis","followers_url":"https://api.github.com/users/kargakis/followers","following_url":"https://api.github.com/users/kargakis/following{/other_user}","gists_url":"https://api.github.com/users/kargakis/gists{/gist_id}","starred_url":"https://api.github.com/users/kargakis/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kargakis/subscriptions","organizations_url":"https://api.github.com/users/kargakis/orgs","repos_url":"https://api.github.com/users/kargakis/repos","events_url":"https://api.github.com/users/kargakis/events{/privacy}","received_events_url":"https://api.github.com/users/kargakis/received_events","type":"User","site_admin":false},{"login":"JacobTanenbaum","id":13948445,"avatar_url":"https://avatars0.githubusercontent.com/u/13948445?v=4","gravatar_id":"","url":"https://api.github.com/users/JacobTanenbaum","html_url":"https://github.com/JacobTanenbaum","followers_url":"https://api.github.com/users/JacobTanenbaum/followers","following_url":"https://api.github.com/users/JacobTanenbaum/following{/other_user}","gists_url":"https://api.github.com/users/JacobTanenbaum/gists{/gist_id}","starred_url":"https://api.github.com/users/JacobTanenbaum/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/JacobTanenbaum/subscriptions","organizations_url":"https://api.github.com/users/JacobTanenbaum/orgs","repos_url":"https://api.github.com/users/JacobTanenbaum/repos","events_url":"https://api.github.com/users/JacobTanenbaum/events{/privacy}","received_events_url":"https://api.github.com/users/JacobTanenbaum/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[{"id":223051294,"url":"https://api.github.com/repos/openshift/origin/labels/approved","name":"approved","color":"0ffa16","default":false},{"id":657292400,"url":"https://api.github.com/repos/openshift/origin/labels/sig/networking","name":"sig/networking","color":"f9d0c4","default":false},{"id":640548662,"url":"https://api.github.com/repos/openshift/origin/labels/size/XXL","name":"size/XXL","color":"ee0000","default":false}],"milestone":null,"commits_url":"https://api.github.com/repos/openshift/origin/pulls/18658/commits","review_comments_url":"https://api.github.com/repos/openshift/origin/pulls/18658/comments","review_comment_url":"https://api.github.com/repos/openshift/origin/pulls/comments{/number}","comments_url":"https://api.github.com/repos/openshift/origin/issues/18658/comments","statuses_url":"https://api.github.com/repos/openshift/origin/statuses/dd0be4c99eb51e8018ed594151fac7f93fc112e1","head":{"label":"smarterclayton:ingress_to_route","ref":"ingress_to_route","sha":"dd0be4c99eb51e8018ed594151fac7f93fc112e1","user":{"login":"smarterclayton","id":1163175,"avatar_url":"https://avatars0.githubusercontent.com/u/1163175?v=4","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},"repo":{"id":22921364,"name":"origin","full_name":"smarterclayton/origin","owner":{"login":"smarterclayton","id":1163175,"avatar_url":"https://avatars0.githubusercontent.com/u/1163175?v=4","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},"private":false,"html_url":"https://github.com/smarterclayton/origin","description":"OpenShift 3 - build, deploy, and manage your applications with Docker and Kubernetes","fork":true,"url":"https://api.github.com/repos/smarterclayton/origin","forks_url":"https://api.github.com/repos/smarterclayton/origin/forks","keys_url":"https://api.github.com/repos/smarterclayton/origin/keys{/key_id}","collaborators_url":"https://api.github.com/repos/smarterclayton/origin/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/smarterclayton/origin/teams","hooks_url":"https://api.github.com/repos/smarterclayton/origin/hooks","issue_events_url":"https://api.github.com/repos/smarterclayton/origin/issues/events{/number}","events_url":"https://api.github.com/repos/smarterclayton/origin/events","assignees_url":"https://api.github.com/repos/smarterclayton/origin/assignees{/user}","branches_url":"https://api.github.com/repos/smarterclayton/origin/branches{/branch}","tags_url":"https://api.github.com/repos/smarterclayton/origin/tags","blobs_url":"https://api.github.com/repos/smarterclayton/origin/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/smarterclayton/origin/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/smarterclayton/origin/git/refs{/sha}","trees_url":"https://api.github.com/repos/smarterclayton/origin/git/trees{/sha}","statuses_url":"https://api.github.com/repos/smarterclayton/origin/statuses/{sha}","languages_url":"https://api.github.com/repos/smarterclayton/origin/languages","stargazers_url":"https://api.github.com/repos/smarterclayton/origin/stargazers","contributors_url":"https://api.github.com/repos/smarterclayton/origin/contributors","subscribers_url":"https://api.github.com/repos/smarterclayton/origin/subscribers","subscription_url":"https://api.github.com/repos/smarterclayton/origin/subscription","commits_url":"https://api.github.com/repos/smarterclayton/origin/commits{/sha}","git_commits_url":"https://api.github.com/repos/smarterclayton/origin/git/commits{/sha}","comments_url":"https://api.github.com/repos/smarterclayton/origin/comments{/number}","issue_comment_url":"https://api.github.com/repos/smarterclayton/origin/issues/comments{/number}","contents_url":"https://api.github.com/repos/smarterclayton/origin/contents/{+path}","compare_url":"https://api.github.com/repos/smarterclayton/origin/compare/{base}...{head}","merges_url":"https://api.github.com/repos/smarterclayton/origin/merges","archive_url":"https://api.github.com/repos/smarterclayton/origin/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/smarterclayton/origin/downloads","issues_url":"https://api.github.com/repos/smarterclayton/origin/issues{/number}","pulls_url":"https://api.github.com/repos/smarterclayton/origin/pulls{/number}","milestones_url":"https://api.github.com/repos/smarterclayton/origin/milestones{/number}","notifications_url":"https://api.github.com/repos/smarterclayton/origin/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/smarterclayton/origin/labels{/name}","releases_url":"https://api.github.com/repos/smarterclayton/origin/releases{/id}","deployments_url":"https://api.github.com/repos/smarterclayton/origin/deployments","created_at":"2014-08-13T15:49:23Z","updated_at":"2017-07-03T18:24:27Z","pushed_at":"2018-03-01T21:38:02Z","git_url":"git://github.com/smarterclayton/origin.git","ssh_url":"git@github.com:smarterclayton/origin.git","clone_url":"https://github.com/smarterclayton/origin.git","svn_url":"https://github.com/smarterclayton/origin","homepage":"https://openshift.github.io","size":904930,"stargazers_count":1,"watchers_count":1,"language":"Go","has_issues":false,"has_projects":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":0,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0"},"forks":0,"open_issues":0,"watchers":1,"default_branch":"master"}},"base":{"label":"openshift:master","ref":"master","sha":"1c7e35223ed75dfc17f37f8b12d3398924d60893","user":{"login":"openshift","id":792337,"avatar_url":"https://avatars3.githubusercontent.com/u/792337?v=4","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://avatars3.githubusercontent.com/u/792337?v=4","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":"Enterprise Kubernetes for Developers","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}","deployments_url":"https://api.github.com/repos/openshift/origin/deployments","created_at":"2014-07-30T20:04:05Z","updated_at":"2018-03-01T15:44:46Z","pushed_at":"2018-03-01T21:38:19Z","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":885949,"stargazers_count":4136,"watchers_count":4136,"language":"Go","has_issues":true,"has_projects":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":2594,"mirror_url":null,"archived":false,"open_issues_count":1514,"license":{"key":"apache-2.0","name":"Apache License 2.0","spdx_id":"Apache-2.0","url":"https://api.github.com/licenses/apache-2.0"},"forks":2594,"open_issues":1514,"watchers":4136,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/openshift/origin/pulls/18658"},"html":{"href":"https://github.com/openshift/origin/pull/18658"},"issue":{"href":"https://api.github.com/repos/openshift/origin/issues/18658"},"comments":{"href":"https://api.github.com/repos/openshift/origin/issues/18658/comments"},"review_comments":{"href":"https://api.github.com/repos/openshift/origin/pulls/18658/comments"},"review_comment":{"href":"https://api.github.com/repos/openshift/origin/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/openshift/origin/pulls/18658/commits"},"statuses":{"href":"https://api.github.com/repos/openshift/origin/statuses/dd0be4c99eb51e8018ed594151fac7f93fc112e1"}},"author_association":"OWNER"}}
{"actor":{"display_login":"smarterclayton"}}