{"action":"created","comment":{"url":"https://api.github.com/repos/openshift/origin/pulls/comments/73104118","id":73104118,"diff_hunk":"@@ -0,0 +1,592 @@\n+package ingressip\n+\n+import (\n+\t\"fmt\"\n+\t\"net\"\n+\t\"sort\"\n+\t\"sync\"\n+\t\"time\"\n+\n+\t\"github.com/golang/glog\"\n+\tkapi \"k8s.io/kubernetes/pkg/api\"\n+\tkerrors \"k8s.io/kubernetes/pkg/api/errors\"\n+\t\"k8s.io/kubernetes/pkg/client/cache\"\n+\tkclient \"k8s.io/kubernetes/pkg/client/unversioned\"\n+\t\"k8s.io/kubernetes/pkg/controller\"\n+\t\"k8s.io/kubernetes/pkg/controller/framework\"\n+\t\"k8s.io/kubernetes/pkg/registry/service/allocator\"\n+\t\"k8s.io/kubernetes/pkg/registry/service/ipallocator\"\n+\t\"k8s.io/kubernetes/pkg/runtime\"\n+\tutilruntime \"k8s.io/kubernetes/pkg/util/runtime\"\n+\t\"k8s.io/kubernetes/pkg/util/wait\"\n+\t\"k8s.io/kubernetes/pkg/util/workqueue\"\n+\t\"k8s.io/kubernetes/pkg/watch\"\n+)\n+\n+const (\n+\t// It's necessary to allocate for the initial sync in consistent\n+\t// order rather than in the order received.  This requires waiting\n+\t// until the initial sync has been processed, and to avoid a hot\n+\t// loop, we'll wait this long between checks.\n+\tSyncProcessedPollPeriod = 100 * time.Millisecond\n+\n+\tclientRetryCount    = 5\n+\tclientRetryInterval = 5 * time.Second\n+)\n+\n+// IngressIPController is responsible for allocating ingress ip\n+// addresses to Service objects of type LoadBalancer.\n+type IngressIPController struct {\n+\tclient kclient.ServicesNamespacer\n+\n+\t// Services that need to be checked\n+\tqueue      workqueue.RateLimitingInterface\n+\tmaxRetries int\n+\n+\tcache      cache.Store\n+\tcontroller *framework.Controller\n+\n+\t// Tracks ip allocation for the configured range\n+\tipAllocator *ipallocator.Range\n+\t// Tracks ip -> service key to allow detection and correction of\n+\t// duplicate ip allocations.\n+\tallocationMap map[string]string\n+\t// Tracks allocations that will be performed after the initial sync\n+\tpendingAllocations []*kapi.Service\n+\n+\t// Protects the transition between initial sync and regular processing\n+\tmu sync.RWMutex\n+\t// Tracks whether the initial sync has been processed\n+\tsyncProcessed bool\n+\n+\t// changeHandler does the work. It's factored out for unit testing\n+\tchangeHandler func(change *serviceChange) error\n+}\n+\n+type serviceChange struct {\n+\tkey        string\n+\toldService *kapi.Service\n+}\n+\n+// NewIngressIPController creates a new IngressIPController.\n+// TODO this should accept a shared informer\n+func NewIngressIPController(client kclient.ServicesNamespacer, ipNet *net.IPNet, resyncInterval time.Duration) *IngressIPController {\n+\tic := &IngressIPController{\n+\t\tclient:     client,\n+\t\tqueue:      workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),\n+\t\tmaxRetries: 10,\n+\t}\n+\n+\tic.cache, ic.controller = framework.NewInformer(\n+\t\t&cache.ListWatch{\n+\t\t\tListFunc: func(options kapi.ListOptions) (runtime.Object, error) {\n+\t\t\t\treturn ic.client.Services(kapi.NamespaceAll).List(options)\n+\t\t\t},\n+\t\t\tWatchFunc: func(options kapi.ListOptions) (watch.Interface, error) {\n+\t\t\t\treturn ic.client.Services(kapi.NamespaceAll).Watch(options)\n+\t\t\t},\n+\t\t},\n+\t\t&kapi.Service{},\n+\t\tresyncInterval,\n+\t\tframework.ResourceEventHandlerFuncs{\n+\t\t\tAddFunc: func(obj interface{}) {\n+\t\t\t\tservice := obj.(*kapi.Service)\n+\t\t\t\tglog.V(4).Infof(\"Adding service %s/%s\", service.Namespace, service.Name)\n+\t\t\t\tic.enqueueChange(obj, nil)\n+\t\t\t},\n+\t\t\tUpdateFunc: func(old, cur interface{}) {\n+\t\t\t\tservice := cur.(*kapi.Service)\n+\t\t\t\tglog.V(4).Infof(\"Updating service %s/%s\", service.Namespace, service.Name)\n+\t\t\t\tic.enqueueChange(cur, old)\n+\t\t\t},\n+\t\t\tDeleteFunc: func(obj interface{}) {\n+\t\t\t\tservice := obj.(*kapi.Service)\n+\t\t\t\tglog.V(4).Infof(\"Deleting service %s/%s\", service.Namespace, service.Name)\n+\t\t\t\tic.enqueueChange(nil, obj)\n+\t\t\t},\n+\t\t},\n+\t)\n+\n+\tic.changeHandler = ic.processChange\n+\n+\tic.ipAllocator = ipallocator.NewAllocatorCIDRRange(ipNet, func(max int, rangeSpec string) allocator.Interface {\n+\t\treturn allocator.NewContiguousAllocationMap(max, rangeSpec)\n+\t})\n+\n+\tic.allocationMap = make(map[string]string)\n+\n+\treturn ic\n+}\n+\n+func (ic *IngressIPController) enqueueChange(new interface{}, old interface{}) {\n+\tic.mu.Lock()\n+\tdefer ic.mu.Unlock()\n+\n+\tchange := &serviceChange{}\n+\n+\tif new != nil {\n+\t\tkey, err := controller.KeyFunc(new)\n+\t\tif err != nil {\n+\t\t\tglog.Errorf(\"Couldn't get key for object %+v: %v\", new, err)\n+\t\t\treturn\n+\t\t}\n+\t\tchange.key = key\n+\t}\n+\n+\tif old != nil {\n+\t\tchange.oldService = old.(*kapi.Service)\n+\t}\n+\n+\tic.queue.Add(change)\n+}\n+\n+// Run begins watching and syncing.\n+func (ic *IngressIPController) Run(stopCh <-chan struct{}) {\n+\tdefer utilruntime.HandleCrash()\n+\tgo ic.controller.Run(stopCh)\n+\n+\tglog.V(4).Infof(\"Waiting for the initial sync to be completed\")\n+\tfor !ic.controller.HasSynced() {\n+\t\tselect {\n+\t\tcase <-time.After(SyncProcessedPollPeriod):\n+\t\tcase <-stopCh:\n+\t\t\treturn\n+\t\t}\n+\t}\n+\n+\tif !ic.processInitialSync() {\n+\t\treturn\n+\t}\n+\n+\tglog.V(4).Infof(\"Starting normal worker\")\n+\tgo wait.Until(ic.worker, time.Second, stopCh)\n+\n+\t<-stopCh\n+\tglog.Infof(\"Shutting down ingress ip controller\")\n+\tic.queue.ShutDown()\n+}\n+\n+type serviceAge []*kapi.Service\n+\n+func (s serviceAge) Len() int      { return len(s) }\n+func (s serviceAge) Swap(i, j int) { s[i], s[j] = s[j], s[i] }\n+func (s serviceAge) Less(i, j int) bool {\n+\tif s[i].CreationTimestamp.Before(s[j].CreationTimestamp) {\n+\t\treturn true\n+\t}\n+\treturn (s[i].CreationTimestamp == s[j].CreationTimestamp && s[i].UID < s[i].UID)\n+}\n+\n+// processInitialSync processes the items queued by informer's initial sync.\n+// A mutex is shared between this method and enqueueService to ensure\n+// that queue additions are blocked while the sync is being processed.\n+// Returns a boolean indication of whether processing should continue.\n+func (ic *IngressIPController) processInitialSync() bool {\n+\tic.mu.Lock()\n+\tdefer ic.mu.Unlock()\n+\n+\tglog.V(4).Infof(\"Processing initial sync\")\n+\n+\t// Drain the initial queue.  Len() should be safe because\n+\t// enqueueService requires the same lock held by this method.\n+\tfor ic.queue.Len() > 0 {\n+\t\tif !ic.work() {\n+\t\t\treturn false\n+\t\t}\n+\t}\n+\n+\t// Add unallocated services back to the queue in consistent order.\n+\tsort.Sort(serviceAge(ic.pendingAllocations))\n+\tfor _, service := range ic.pendingAllocations {\n+\t\tkey, err := controller.KeyFunc(service)\n+\t\tif err != nil {\n+\t\t\t// This error should have been caught by enqueueService\n+\t\t\tglog.Errorf(\"Couldn't get key for service %+v: %v\", service, err)\n+\t\t\tcontinue\n+\t\t}\n+\t\tglog.V(4).Infof(\"Adding service to allocation queue: %v \", key)\n+\t\tchange := &serviceChange{key: key}\n+\t\tic.queue.Add(change)\n+\t}\n+\n+\tic.pendingAllocations = nil\n+\tic.syncProcessed = true\n+\n+\tglog.V(4).Infof(\"Completed processing initial sync\")\n+\n+\treturn true\n+}\n+\n+// worker runs a worker thread that dequeues items, processes them,\n+// and marks them done.\n+func (ic *IngressIPController) worker() {\n+\tfor {\n+\t\tif !ic.work() {\n+\t\t\treturn\n+\t\t}\n+\t}\n+}\n+\n+// work returns true if the worker thread should continue\n+func (ic *IngressIPController) work() bool {\n+\tchange, quit := ic.queue.Get()\n+\tif quit {\n+\t\treturn false\n+\t}\n+\tdefer ic.queue.Done(change)\n+\n+\tif err := ic.changeHandler(change.(*serviceChange)); err == nil {\n+\t\t// No further processing required\n+\t\tic.queue.Forget(change)\n+\t} else {\n+\t\t// Failed but can be retried\n+\t\tutilruntime.HandleError(fmt.Errorf(\"error syncing service, it will be retried: %v\", err))\n+\t\tic.queue.AddRateLimited(change)\n+\t}\n+\n+\treturn true\n+}\n+\n+// processChange responds to a service change\n+// (addition/update/deletion) by synchronizing the local and persisted\n+// ingress ip allocation state of the service.\n+func (ic *IngressIPController) processChange(change *serviceChange) error {\n+\tvar service *kapi.Service\n+\tif change.key != \"\" {\n+\t\tobj, exists, err := ic.cache.GetByKey(change.key)\n+\t\tif err != nil {\n+\t\t\tglog.V(4).Infof(\"Unable to retrieve service %v from store: %v\", change.key, err)\n+\t\t} else if !exists {\n+\t\t\tglog.V(4).Infof(\"Service has been deleted %v\", change.key)\n+\t\t} else {\n+\t\t\tservice = obj.(*kapi.Service)\n+\t\t}\n+\t}\n+\n+\toldIP := \"\"\n+\tif change.oldService != nil && change.oldService.Spec.Type == kapi.ServiceTypeLoadBalancer {\n+\t\tingress := change.oldService.Status.LoadBalancer.Ingress\n+\t\tif len(ingress) > 0 {\n+\t\t\toldIP = ingress[0].IP\n+\t\t}\n+\t}\n+\n+\tif service != nil {\n+\t\tif service.Spec.Type == kapi.ServiceTypeLoadBalancer {\n+\t\t\tif len(service.Status.LoadBalancer.Ingress) > 0 {\n+\t\t\t\t// Service with existing allocation\n+\t\t\t\treturn ic.recordAllocation(service, change.key, oldIP)\n+\t\t\t} else {\n+\t\t\t\t// Service requiring allocation\n+\t\t\t\t// TODO is the oldip relevant here?\n+\t\t\t\treturn ic.allocate(service, change.key)\n+\t\t\t}\n+\t\t} else {\n+\t\t\t// A service for which allocation status exists\n+\t\t\t// but that is no longer of type LoadBalancer\n+\t\t\t// should be deallocated.\n+\t\t\tif len(service.Status.LoadBalancer.Ingress) > 0 {\n+\t\t\t\t// TODO is the oldip relevant here?\n+\t\t\t\treturn ic.deallocate(service, change.key)\n+\t\t\t}\n+\t\t}\n+\t} else if oldIP != \"\" {\n+\t\t// Assume deletion if no service is present but an old ip exists.\n+\t\tkey, err := controller.KeyFunc(change.oldService)\n+\t\tif err != nil {\n+\t\t\t// Recovery/retry not possible for this error\n+\t\t\tglog.Errorf(\"Couldn't get key for object %+v: %v\", change.oldService, err)\n+\t\t\treturn nil\n+\t\t}\n+\t\tic.clearLocalAllocation(key, oldIP)\n+\t}\n+\n+\treturn nil\n+}\n+\n+// clearLocalAllocation clears an in-memory allocation if it belongs\n+// to the specified service key.\n+func (ic *IngressIPController) clearLocalAllocation(key, ipString string) {\n+\tip := net.ParseIP(ipString)\n+\tif ip == nil {\n+\t\t// An invalid ip address cannot be deallocated\n+\t\tglog.Errorf(\"Error parsing ip: %v\", ipString)\n+\t\treturn\n+\t}\n+\n+\tipKey, ok := ic.allocationMap[ipString]\n+\tif !ok {\n+\t\t// IP is not currently allocated\n+\t\treturn\n+\t}\n+\tif key != ipKey {\n+\t\t// IP is not allocated to the service\n+\t\treturn\n+\t}\n+\n+\t// Remove allocation\n+\terr := ic.ipAllocator.Release(ip)\n+\tif err != nil {\n+\t\t// Release from contiguous allocator should never return an error.\n+\t\tglog.Errorf(\"Error releasing ip %v for service %v: %v\", ipString, key, err)\n+\t\treturn\n+\t}\n+\tdelete(ic.allocationMap, ipString)\n+}\n+\n+// recordAllocation updates local state with the ingress ip indicated\n+// in a service's status and ensures that the ingress ip appears in\n+// the service's list of external ips.\n+func (ic *IngressIPController) recordAllocation(service *kapi.Service, key, oldIP string) error {\n+\t// If more than one ingress ip is present, it will be ignored\n+\tipString := service.Status.LoadBalancer.Ingress[0].IP\n+\n+\t// Clear a previously allocated ip\n+\tif oldIP != \"\" && ipString != oldIP {\n+\t\tic.clearLocalAllocation(key, oldIP)\n+\t}\n+\n+\t// Make a copy to modify to avoid mutating cache state\n+\tt, err := kapi.Scheme.DeepCopy(service)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\tsCopy := t.(*kapi.Service)\n+\n+\tip := net.ParseIP(ipString)\n+\tif ip == nil {\n+\t\t// An invalid ingress ip is assigned to the service.\n+\t\tif !ic.syncProcessed {\n+\t\t\t// Fix the problem after processing the initial sync to\n+\t\t\t// avoid delaying cache updates any longer than necessary.\n+\t\t\tic.pendingAllocations = append(ic.pendingAllocations, service)\n+\t\t\treturn nil\n+\t\t} else {\n+\t\t\treturn ic.clearPersistedAllocation(sCopy)\n+\t\t}\n+\t}\n+\n+\tipKey, ok := ic.allocationMap[ipString]\n+\tif ok && ipKey == key {\n+\t\t// Allocation is for this service.\n+\t\tif !ic.syncProcessed {\n+\t\t\t// Avoid ensuring an external ip until after the initial sync.\n+\t\t\tic.pendingAllocations = append(ic.pendingAllocations, service)\n+\t\t\treturn nil\n+\t\t} else {\n+\t\t\t// Ensure that the ingress ip is present in the service's spec.\n+\t\t\treturn ic.ensureExternalIP(sCopy, ipString)\n+\t\t}\n+\t}\n+\n+\terr = ic.ipAllocator.Allocate(ip)\n+\tif err != nil {\n+\t\tif !ic.syncProcessed {\n+\t\t\t// Fix the problem after processing the initial sync\n+\t\t\tic.pendingAllocations = append(ic.pendingAllocations, service)\n+\t\t\treturn nil\n+\t\t} else {\n+\t\t\t// Clear the allocation so the service can be re-allocated\n+\t\t\t// when the update is seen.\n+\t\t\treturn ic.clearPersistedAllocation(sCopy)\n+\t\t}\n+\t}\n+\tic.allocationMap[ipString] = key\n+\tif !ic.syncProcessed {\n+\t\t// Avoid ensuring an external ip until after the initial sync.\n+\t\tic.pendingAllocations = append(ic.pendingAllocations, service)\n+\t\treturn nil\n+\t} else {\n+\t\t// Ensure that the ingress ip is present in the service's spec.\n+\t\treturn ic.ensureExternalIP(sCopy, ipString)\n+\t}\n+}\n+\n+// clearPersistedAllocation ensures there is no ingress ip in the\n+// service's spec and that the service's status is cleared.\n+func (ic *IngressIPController) clearPersistedAllocation(service *kapi.Service) error {\n+\t// Assume it is safe to modify the service without worrying about changing the local cache\n+\n+\t// An ingress ip is only allowed in ExternalIPs when a\n+\t// corresponding status exists, so update the spec first to avoid\n+\t// failing admission control.\n+\tingressIP := service.Status.LoadBalancer.Ingress[0].IP\n+\n+\tfilteredIPs := service.Spec.ExternalIPs[:0]\n+\tchanged := false\n+\tfor _, ip := range service.Spec.ExternalIPs {\n+\t\tif ip != ingressIP {\n+\t\t\tfilteredIPs = append(filteredIPs, ip)\n+\t\t} else {\n+\t\t\tchanged = true\n+\t\t}\n+\t}\n+\tif changed {\n+\t\tservice.Spec.ExternalIPs = filteredIPs\n+\t\terr := ic.persistSpec(service)\n+\t\tif err != nil {\n+\t\t\treturn err\n+\t\t}\n+\t}\n+\n+\tservice.Status.LoadBalancer = kapi.LoadBalancerStatus{}\n+\treturn ic.persistStatus(service)\n+}\n+\n+// allocate assigns an unallocated ip to the given service and updates\n+// the service's persisted state.\n+func (ic *IngressIPController) allocate(service *kapi.Service, key string) error {\n+\tif !ic.syncProcessed {\n+\t\tic.pendingAllocations = append(ic.pendingAllocations, service)\n+\t\treturn nil\n+\t}\n+\n+\t// Make a copy to avoid mutating cache state\n+\tt, err := kapi.Scheme.DeepCopy(service)\n+\tif err != nil {\n+\t\treturn err\n+\t}\n+\tsCopy := t.(*kapi.Service)","path":"pkg/service/controller/ingressip/controller.go","position":354,"original_position":449,"commit_id":"d9ba22a6fc7d91c617d52a922851c4c6acdbb65e","original_commit_id":"d9ba22a6fc7d91c617d52a922851c4c6acdbb65e","user":{"login":"marun","id":451477,"avatar_url":"https://avatars.githubusercontent.com/u/451477?v=3","gravatar_id":"","url":"https://api.github.com/users/marun","html_url":"https://github.com/marun","followers_url":"https://api.github.com/users/marun/followers","following_url":"https://api.github.com/users/marun/following{/other_user}","gists_url":"https://api.github.com/users/marun/gists{/gist_id}","starred_url":"https://api.github.com/users/marun/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/marun/subscriptions","organizations_url":"https://api.github.com/users/marun/orgs","repos_url":"https://api.github.com/users/marun/repos","events_url":"https://api.github.com/users/marun/events{/privacy}","received_events_url":"https://api.github.com/users/marun/received_events","type":"User","site_admin":false},"body":"Done","created_at":"2016-08-02T07:11:13Z","updated_at":"2016-08-02T07:11:13Z","html_url":"https://github.com/openshift/origin/pull/9454#discussion_r73104118","pull_request_url":"https://api.github.com/repos/openshift/origin/pulls/9454","_links":{"self":{"href":"https://api.github.com/repos/openshift/origin/pulls/comments/73104118"},"html":{"href":"https://github.com/openshift/origin/pull/9454#discussion_r73104118"},"pull_request":{"href":"https://api.github.com/repos/openshift/origin/pulls/9454"}}},"pull_request":{"url":"https://api.github.com/repos/openshift/origin/pulls/9454","id":74554449,"html_url":"https://github.com/openshift/origin/pull/9454","diff_url":"https://github.com/openshift/origin/pull/9454.diff","patch_url":"https://github.com/openshift/origin/pull/9454.patch","issue_url":"https://api.github.com/repos/openshift/origin/issues/9454","number":9454,"state":"open","locked":false,"title":"WIP: Support network ingress on arbitrary ports","user":{"login":"marun","id":451477,"avatar_url":"https://avatars.githubusercontent.com/u/451477?v=3","gravatar_id":"","url":"https://api.github.com/users/marun","html_url":"https://github.com/marun","followers_url":"https://api.github.com/users/marun/followers","following_url":"https://api.github.com/users/marun/following{/other_user}","gists_url":"https://api.github.com/users/marun/gists{/gist_id}","starred_url":"https://api.github.com/users/marun/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/marun/subscriptions","organizations_url":"https://api.github.com/users/marun/orgs","repos_url":"https://api.github.com/users/marun/repos","events_url":"https://api.github.com/users/marun/events{/privacy}","received_events_url":"https://api.github.com/users/marun/received_events","type":"User","site_admin":false},"body":"Add a new ExternalIP type to enable tracking of allocated IPs and make it possible to have an external address be allocated to a service automatically from the range indicated by ExternalIPNetworkCIDRs. The type is global in scope with the name an ip address to ensure an ip is allocated at most once. For simplicity, an IP will be dedicated to a single service rather than having the port space of an ip be shared between many services.\r\n\r\nTODO:\r\n - Decide what policy and quota should be applied to the new type\r\n - Prevent deletion of an ExternalIP if a service is still using it\r\n - Delete ExternalIPs that are no longer referenced by a service\r\n - ~~Update the admission controller~~ Done\r\n    - ~~Return an error if a requested external ip is already in use~~\r\n    - ~~Create a new ExternalIP object for each requested external ip~~\r\n - Add support for automatically allocating an external ip for a service that defines an appropriate annotation (e.g. allocateExternalIP)\r\n   - Store the automatically allocated ip address in the service's externalIP list.  This should eventually be replaced by ingress and use of a status field to separate spec/intent from cluster state.  For now, provide a degree of separation by supporting the allocation of a single address and refusing to allocate if externalIPs are specified statically.  \r\n\r\n\r\ncc: @openshift/networking @smarterclayton ","created_at":"2016-06-21T05:47:00Z","updated_at":"2016-08-02T07:11:13Z","closed_at":null,"merged_at":null,"merge_commit_sha":"f937cee52d66c31e2d8010628108ec8e137e6872","assignee":null,"assignees":[],"milestone":null,"commits_url":"https://api.github.com/repos/openshift/origin/pulls/9454/commits","review_comments_url":"https://api.github.com/repos/openshift/origin/pulls/9454/comments","review_comment_url":"https://api.github.com/repos/openshift/origin/pulls/comments{/number}","comments_url":"https://api.github.com/repos/openshift/origin/issues/9454/comments","statuses_url":"https://api.github.com/repos/openshift/origin/statuses/d9ba22a6fc7d91c617d52a922851c4c6acdbb65e","head":{"label":"marun:tcp-routes","ref":"tcp-routes","sha":"d9ba22a6fc7d91c617d52a922851c4c6acdbb65e","user":{"login":"marun","id":451477,"avatar_url":"https://avatars.githubusercontent.com/u/451477?v=3","gravatar_id":"","url":"https://api.github.com/users/marun","html_url":"https://github.com/marun","followers_url":"https://api.github.com/users/marun/followers","following_url":"https://api.github.com/users/marun/following{/other_user}","gists_url":"https://api.github.com/users/marun/gists{/gist_id}","starred_url":"https://api.github.com/users/marun/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/marun/subscriptions","organizations_url":"https://api.github.com/users/marun/orgs","repos_url":"https://api.github.com/users/marun/repos","events_url":"https://api.github.com/users/marun/events{/privacy}","received_events_url":"https://api.github.com/users/marun/received_events","type":"User","site_admin":false},"repo":{"id":36902865,"name":"origin","full_name":"marun/origin","owner":{"login":"marun","id":451477,"avatar_url":"https://avatars.githubusercontent.com/u/451477?v=3","gravatar_id":"","url":"https://api.github.com/users/marun","html_url":"https://github.com/marun","followers_url":"https://api.github.com/users/marun/followers","following_url":"https://api.github.com/users/marun/following{/other_user}","gists_url":"https://api.github.com/users/marun/gists{/gist_id}","starred_url":"https://api.github.com/users/marun/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/marun/subscriptions","organizations_url":"https://api.github.com/users/marun/orgs","repos_url":"https://api.github.com/users/marun/repos","events_url":"https://api.github.com/users/marun/events{/privacy}","received_events_url":"https://api.github.com/users/marun/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/marun/origin","description":"OpenShift 3 - build, deploy, and manage your applications with Docker and Kubernetes","fork":true,"url":"https://api.github.com/repos/marun/origin","forks_url":"https://api.github.com/repos/marun/origin/forks","keys_url":"https://api.github.com/repos/marun/origin/keys{/key_id}","collaborators_url":"https://api.github.com/repos/marun/origin/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/marun/origin/teams","hooks_url":"https://api.github.com/repos/marun/origin/hooks","issue_events_url":"https://api.github.com/repos/marun/origin/issues/events{/number}","events_url":"https://api.github.com/repos/marun/origin/events","assignees_url":"https://api.github.com/repos/marun/origin/assignees{/user}","branches_url":"https://api.github.com/repos/marun/origin/branches{/branch}","tags_url":"https://api.github.com/repos/marun/origin/tags","blobs_url":"https://api.github.com/repos/marun/origin/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/marun/origin/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/marun/origin/git/refs{/sha}","trees_url":"https://api.github.com/repos/marun/origin/git/trees{/sha}","statuses_url":"https://api.github.com/repos/marun/origin/statuses/{sha}","languages_url":"https://api.github.com/repos/marun/origin/languages","stargazers_url":"https://api.github.com/repos/marun/origin/stargazers","contributors_url":"https://api.github.com/repos/marun/origin/contributors","subscribers_url":"https://api.github.com/repos/marun/origin/subscribers","subscription_url":"https://api.github.com/repos/marun/origin/subscription","commits_url":"https://api.github.com/repos/marun/origin/commits{/sha}","git_commits_url":"https://api.github.com/repos/marun/origin/git/commits{/sha}","comments_url":"https://api.github.com/repos/marun/origin/comments{/number}","issue_comment_url":"https://api.github.com/repos/marun/origin/issues/comments{/number}","contents_url":"https://api.github.com/repos/marun/origin/contents/{+path}","compare_url":"https://api.github.com/repos/marun/origin/compare/{base}...{head}","merges_url":"https://api.github.com/repos/marun/origin/merges","archive_url":"https://api.github.com/repos/marun/origin/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/marun/origin/downloads","issues_url":"https://api.github.com/repos/marun/origin/issues{/number}","pulls_url":"https://api.github.com/repos/marun/origin/pulls{/number}","milestones_url":"https://api.github.com/repos/marun/origin/milestones{/number}","notifications_url":"https://api.github.com/repos/marun/origin/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/marun/origin/labels{/name}","releases_url":"https://api.github.com/repos/marun/origin/releases{/id}","deployments_url":"https://api.github.com/repos/marun/origin/deployments","created_at":"2015-06-05T00:21:35Z","updated_at":"2015-06-05T00:21:38Z","pushed_at":"2016-07-29T15:21:26Z","git_url":"git://github.com/marun/origin.git","ssh_url":"git@github.com:marun/origin.git","clone_url":"https://github.com/marun/origin.git","svn_url":"https://github.com/marun/origin","homepage":"http://www.openshift.org","size":325614,"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":"10133fac053fb3c68764c716406153c0123aec2a","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":"Enterprise-Ready 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":"2016-08-02T05:02:26Z","pushed_at":"2016-08-02T06:28:58Z","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":554381,"stargazers_count":1529,"watchers_count":1529,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":931,"mirror_url":null,"open_issues_count":1107,"forks":931,"open_issues":1107,"watchers":1529,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/openshift/origin/pulls/9454"},"html":{"href":"https://github.com/openshift/origin/pull/9454"},"issue":{"href":"https://api.github.com/repos/openshift/origin/issues/9454"},"comments":{"href":"https://api.github.com/repos/openshift/origin/issues/9454/comments"},"review_comments":{"href":"https://api.github.com/repos/openshift/origin/pulls/9454/comments"},"review_comment":{"href":"https://api.github.com/repos/openshift/origin/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/openshift/origin/pulls/9454/commits"},"statuses":{"href":"https://api.github.com/repos/openshift/origin/statuses/d9ba22a6fc7d91c617d52a922851c4c6acdbb65e"}}}}
{"actor":{"display_login":"marun"}}