{"action":"created","comment":{"url":"https://api.github.com/repos/openshift/origin/pulls/comments/45062004","id":45062004,"diff_hunk":"@@ -54,230 +51,114 @@ func (e transientError) Error() string {\n \treturn \"transient error handling deployment config: \" + string(e)\n }\n \n-// Handle processes config and creates a new deployment if necessary.\n-func (c *DeploymentConfigController) Handle(config *deployapi.DeploymentConfig) error {\n-\t// TODO(danmace): Don't use findDetails yet. See: https://github.com/openshift/origin/pull/5530\n+func NewDeploymentConfigController(kubeClient kclient.Interface, codec runtime.Codec, recorder record.EventRecorder) *DeploymentConfigController {\n+\treturn &DeploymentConfigController{\n+\t\tkubeClient: kubeClient,\n+\t\tcodec:      codec,\n+\t\trecorder:   recorder,\n+\t}\n+}\n \n-\t// Only deploy when the version has advanced past 0.\n+func (c *DeploymentConfigController) Handle(config *deployapi.DeploymentConfig) error {\n+\t// There's nothing to reconcile until the version is nonzero.\n \tif config.LatestVersion == 0 {\n \t\tglog.V(5).Infof(\"Waiting for first version of %s\", deployutil.LabelForDeploymentConfig(config))\n \t\treturn nil\n \t}\n \n-\texistingDeployments, err := c.deploymentClient.listDeploymentsForConfig(config.Namespace, config.Name)\n+\t// Find all deployments owned by the deploymentConfig.\n+\tsel := deployutil.ConfigSelector(config.Name)\n+\texistingDeployments, err := c.kubeClient.ReplicationControllers(config.Namespace).List(sel, fields.Everything())\n \tif err != nil {\n \t\treturn err\n \t}\n \n-\tvar inflightDeployment *kapi.ReplicationController\n-\tfor _, deployment := range existingDeployments.Items {\n-\n-\t\tdeploymentStatus := deployutil.DeploymentStatusFor(&deployment)\n-\t\tswitch deploymentStatus {\n-\t\tcase deployapi.DeploymentStatusFailed,\n-\t\t\tdeployapi.DeploymentStatusComplete:\n-\t\t\t// Previous deployment in terminal state - can ignore\n-\t\t\t// Ignoring specific deployment states so that any newly introduced\n-\t\t\t// deployment state will not be ignored\n-\t\tdefault:\n-\t\t\tif inflightDeployment == nil {\n-\t\t\t\tinflightDeployment = &deployment\n+\tlatestDeploymentExists, latestDeployment := deployutil.LatestDeploymentInfo(config, existingDeployments)\n+\t// If the latest deployment doesn't exist yet, cancel any running\n+\t// deployments to allow them to be superceded by the new config version.\n+\tinflightDeploymentsExist := false\n+\tif !latestDeploymentExists {\n+\t\tfor _, deployment := range existingDeployments.Items {\n+\t\t\t// Skip deployments with an outcome.\n+\t\t\tif deployutil.IsTerminatedDeployment(&deployment) {\n \t\t\t\tcontinue\n \t\t\t}\n-\t\t\tvar deploymentForCancellation *kapi.ReplicationController\n-\t\t\tif deployutil.DeploymentVersionFor(inflightDeployment) < deployutil.DeploymentVersionFor(&deployment) {\n-\t\t\t\tdeploymentForCancellation, inflightDeployment = inflightDeployment, &deployment\n-\t\t\t} else {\n-\t\t\t\tdeploymentForCancellation = &deployment\n-\t\t\t}\n-\n-\t\t\tdeploymentForCancellation.Annotations[deployapi.DeploymentCancelledAnnotation] = deployapi.DeploymentCancelledAnnotationValue\n-\t\t\tdeploymentForCancellation.Annotations[deployapi.DeploymentStatusReasonAnnotation] = deployapi.DeploymentCancelledNewerDeploymentExists\n-\t\t\tif _, err := c.deploymentClient.updateDeployment(deploymentForCancellation.Namespace, deploymentForCancellation); err != nil {\n-\t\t\t\tutil.HandleError(fmt.Errorf(\"couldn't cancel deployment %s: %v\", deployutil.LabelForDeployment(deploymentForCancellation), err))\n+\t\t\t// Cancel running deployments.\n+\t\t\tinflightDeploymentsExist = true\n+\t\t\tif !deployutil.IsDeploymentCancelled(&deployment) {\n+\t\t\t\tdeployment.Annotations[deployapi.DeploymentCancelledAnnotation] = deployapi.DeploymentCancelledAnnotationValue\n+\t\t\t\tdeployment.Annotations[deployapi.DeploymentStatusReasonAnnotation] = deployapi.DeploymentCancelledNewerDeploymentExists\n+\t\t\t\t_, err := c.kubeClient.ReplicationControllers(deployment.Namespace).Update(&deployment)\n+\t\t\t\tif err != nil {\n+\t\t\t\t\tc.recorder.Eventf(config, \"DeploymentCancellationFailed\", \"Failed to cancel deployment %q superceded by version %d: %s\", deployment.Name, config.LatestVersion, err)\n+\t\t\t\t} else {\n+\t\t\t\t\tc.recorder.Eventf(config, \"DeploymentCancelled\", \"Cancelled deployment %q superceded by version %d\", deployment.Name, config.LatestVersion)\n+\t\t\t\t}\n \t\t\t}\n-\t\t\tglog.V(4).Infof(\"Cancelled deployment %s for deployment config %s\", deployutil.LabelForDeployment(deploymentForCancellation), deployutil.LabelForDeploymentConfig(config))\n \t\t}\n \t}\n-\n-\t// if the latest deployment exists then nothing else needs to be done\n-\tlatestDeploymentExists, _ := deployutil.LatestDeploymentInfo(config, existingDeployments)\n-\tif latestDeploymentExists {\n-\t\treturn nil\n-\t}\n-\n-\t// check to see if there are inflight deployments\n-\tif inflightDeployment != nil {\n+\t// Wait for deployment cancellations before doing any more scaling or\n+\t// creating a new deployment to avoid competing with existing deployment\n+\t// processes.\n+\tif inflightDeploymentsExist {\n+\t\tc.recorder.Eventf(config, \"DeploymentAwaitingCancellation\", \"Deployment of version %d awaiting cancellation of older running deployments\", config.LatestVersion)\n \t\t// raise a transientError so that the deployment config can be re-queued\n-\t\tglog.V(4).Infof(\"Found previous inflight deployment for %s - will requeue\", deployutil.LabelForDeploymentConfig(config))\n \t\treturn transientError(fmt.Sprintf(\"found previous inflight deployment for %s - requeuing\", deployutil.LabelForDeploymentConfig(config)))\n \t}\n-\n-\t// Try and build a deployment for the config.\n-\tdeployment, err := c.makeDeployment(config)\n-\tif err != nil {\n-\t\treturn fatalError(fmt.Sprintf(\"couldn't make deployment from (potentially invalid) deployment config %s: %v\", deployutil.LabelForDeploymentConfig(config), err))\n-\t}\n-\n-\t// Compute the desired replicas for the deployment. Use the last completed\n-\t// deployment's current replica count, or the config template if there is no\n-\t// prior completed deployment available.\n-\tdesiredReplicas := config.Template.ControllerTemplate.Replicas\n-\tif len(existingDeployments.Items) > 0 {\n-\t\tsort.Sort(deployutil.ByLatestVersionDesc(existingDeployments.Items))\n-\t\tfor _, existing := range existingDeployments.Items {\n-\t\t\tif deployutil.DeploymentStatusFor(&existing) == deployapi.DeploymentStatusComplete {\n-\t\t\t\tdesiredReplicas = existing.Spec.Replicas\n-\t\t\t\tglog.V(4).Infof(\"Desired replicas for %s set to %d based on prior completed deployment %s\", deployutil.LabelForDeploymentConfig(config), desiredReplicas, existing.Name)\n-\t\t\t\tbreak\n-\t\t\t}\n-\t\t}\n-\t}\n-\tdeployment.Annotations[deployapi.DesiredReplicasAnnotation] = strconv.Itoa(desiredReplicas)\n-\n-\t// Create the deployment.\n-\tif _, err := c.deploymentClient.createDeployment(config.Namespace, deployment); err == nil {\n-\t\tglog.V(4).Infof(\"Created deployment for deployment config %s\", deployutil.LabelForDeploymentConfig(config))\n-\t\treturn nil\n-\t} else {\n-\t\t// If the deployment was already created, just move on. The cache could be stale, or another\n-\t\t// process could have already handled this update.\n-\t\tif errors.IsAlreadyExists(err) {\n-\t\t\tglog.V(4).Infof(\"Deployment already exists for deployment config %s\", deployutil.LabelForDeploymentConfig(config))\n+\t// If the latest deployment already exists, reconcile replica counts and\n+\t// return early.\n+\tif latestDeploymentExists {\n+\t\t// If the latest deployment exists and is still running, return early and\n+\t\t// reconcile later. We don't want to compete with the deployer.\n+\t\tif !deployutil.IsTerminatedDeployment(latestDeployment) {\n \t\t\treturn nil\n \t\t}\n-\n-\t\tglog.Warningf(\"Cannot create latest deployment for deployment config %q: %v\", deployutil.LabelForDeploymentConfig(config), err)\n-\t\treturn fmt.Errorf(\"couldn't create deployment for deployment config %s: %v\", deployutil.LabelForDeploymentConfig(config), err)\n-\t}\n-}\n-\n-// deploymentClient abstracts access to deployments.\n-type deploymentClient interface {\n-\tcreateDeployment(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error)\n-\t// listDeploymentsForConfig should return deployments associated with the\n-\t// provided config.\n-\tlistDeploymentsForConfig(namespace, configName string) (*kapi.ReplicationControllerList, error)\n-\tupdateDeployment(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error)\n-}\n-\n-// deploymentClientImpl is a pluggable deploymentClient.\n-type deploymentClientImpl struct {\n-\tcreateDeploymentFunc         func(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error)\n-\tlistDeploymentsForConfigFunc func(namespace, configName string) (*kapi.ReplicationControllerList, error)\n-\tupdateDeploymentFunc         func(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error)\n-}\n-\n-func (i *deploymentClientImpl) createDeployment(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error) {\n-\treturn i.createDeploymentFunc(namespace, deployment)\n-}\n-\n-func (i *deploymentClientImpl) listDeploymentsForConfig(namespace, configName string) (*kapi.ReplicationControllerList, error) {\n-\treturn i.listDeploymentsForConfigFunc(namespace, configName)\n-}\n-\n-func (i *deploymentClientImpl) updateDeployment(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error) {\n-\treturn i.updateDeploymentFunc(namespace, deployment)\n-}\n-\n-// findDetails inspects the given deployment configuration for any failure causes\n-// and returns any found details about it\n-// TODO(danmace): Don't use findDetails yet. See: https://github.com/openshift/origin/pull/5530\n-func (c *DeploymentConfigController) findDetails(config *deployapi.DeploymentConfig) (string, *kapi.ReplicationControllerList, bool, error) {\n-\t// Check if any existing inflight deployments (any non-terminal state).\n-\texistingDeployments, err := c.deploymentClient.listDeploymentsForConfig(config.Namespace, config.Name)\n-\tif err != nil {\n-\t\treturn \"\", nil, false, fmt.Errorf(\"couldn't list deployments for deployment config %q: %v\", deployutil.LabelForDeploymentConfig(config), err)\n-\t}\n-\t// check if the latest deployment exists\n-\t// we'll return after we've dealt with the multiple-active-deployments case\n-\tlatestDeploymentExists, latestDeploymentStatus := deployutil.LatestDeploymentInfo(config, existingDeployments)\n-\tif latestDeploymentExists && latestDeploymentStatus != deployapi.DeploymentStatusFailed {\n-\t\t// If the latest deployment exists and is not failed, clear the dc message\n-\t\treturn \"\", existingDeployments, latestDeploymentExists, nil\n-\t}\n-\n-\t// Rest of the code will handle non-existing or failed latest deployment causes\n-\t// TODO: Inspect pod logs in case of failed latest\n-\n-\tdetails := \"\"\n-\tallDetails := []string{}\n-\tif config.Details != nil && len(config.Details.Message) > 0 {\n-\t\t// Populate details with the previous message so that in case we stumble upon\n-\t\t// an unexpected client error, the message won't be overwritten\n-\t\tdetails = config.Details.Message\n-\t}\n-\t// Look into image change triggers and find out possible deployment failures such as\n-\t// missing image stream tags with or without build configurations pointing at them\n-\tfor _, trigger := range config.Triggers {\n-\t\tif trigger.Type != deployapi.DeploymentTriggerOnImageChange || trigger.ImageChangeParams == nil || !trigger.ImageChangeParams.Automatic {\n-\t\t\tcontinue\n-\t\t}\n-\t\tname := trigger.ImageChangeParams.From.Name\n-\t\ttag := trigger.ImageChangeParams.Tag\n-\t\tistag := imageapi.JoinImageStreamTag(name, tag)\n-\n-\t\t// Check if the image stream tag pointed by the trigger exists\n-\t\tif _, err := c.osClient.ImageStreamTags(config.Namespace).Get(name, tag); err != nil {\n-\t\t\tif !errors.IsNotFound(err) {\n-\t\t\t\tglog.V(2).Infof(\"Error while trying to get image stream tag %q: %v\", istag, err)\n-\t\t\t\treturn details, existingDeployments, latestDeploymentExists, nil\n-\t\t\t}\n-\t\t\t// In case the image stream tag was not found, then it either doesn't exist or doesn't exist yet\n-\t\t\t// (a build configuration output points to it so it's going to be populated at some point in the\n-\t\t\t// future)\n-\t\t\tdetails = fmt.Sprintf(\"The image trigger for image stream tag %q will have no effect because image stream tag %q does not exist.\", istag, istag)\n-\t\t\tbcList, err := c.osClient.BuildConfigs(kapi.NamespaceAll).List(labels.Everything(), fields.Everything())\n-\t\t\tif err != nil {\n-\t\t\t\tglog.V(2).Infof(\"Error while trying to list build configs: %v\", err)\n-\t\t\t\treturn details, existingDeployments, latestDeploymentExists, nil\n-\t\t\t}\n-\n-\t\t\tfor _, bc := range bcList.Items {\n-\t\t\t\tif bc.Spec.Output.To != nil && bc.Spec.Output.To.Kind == \"ImageStreamTag\" {\n-\t\t\t\t\tparts := strings.Split(bc.Spec.Output.To.Name, \":\")\n-\t\t\t\t\tif len(parts) != 2 {\n-\t\t\t\t\t\tglog.V(2).Infof(\"Invalid image stream tag: %q\", bc.Spec.Output.To.Name)\n-\t\t\t\t\t\treturn details, existingDeployments, latestDeploymentExists, nil\n-\t\t\t\t\t}\n-\t\t\t\t\tif parts[0] == name && parts[1] == tag {\n-\t\t\t\t\t\tdetails = fmt.Sprintf(\"The image trigger for image stream tag %q will have no effect because image stream tag %q does not exist.\\n\\tIf image stream tag %q is expected, check build config %q which produces image stream tag %q.\", istag, istag, istag, bc.Name, istag)\n-\t\t\t\t\t\tbreak\n-\t\t\t\t\t}\n-\t\t\t\t}\n+\t\t// Reconcile existing deployment replica counts which could have diverged\n+\t\t// outside the deployment process (e.g. due to auto or manual scaling).\n+\t\t// The active deployment is the last successful deployment, not\n+\t\t// necessarily the latest in terms of the config version. If the latest\n+\t\t// version failed to deploy, it should be scaled to zero and the last\n+\t\t// successful should be scaled back up.\n+\t\tactiveDeployment := deployutil.ActiveDeployment(config, existingDeployments)\n+\t\tfor _, deployment := range existingDeployments.Items {\n+\t\t\toldReplicaCount := deployment.Spec.Replicas\n+\t\t\tnewReplicaCount := oldReplicaCount\n+\t\t\t// The active deployment should be scaled to the config replica count, and\n+\t\t\t// everything else should be at zero.\n+\t\t\tif activeDeployment != nil && deployment.Name == activeDeployment.Name {\n+\t\t\t\tnewReplicaCount = config.Template.ControllerTemplate.Replicas\n+\t\t\t} else {\n+\t\t\t\tnewReplicaCount = 0\n \t\t\t}\n-\t\t\t// Try to see if the image stream exists, if not then the build will never be able to update the\n-\t\t\t// tag in question\n-\t\t\tif _, err := c.osClient.ImageStreams(config.Namespace).Get(name); err != nil {\n-\t\t\t\tglog.V(2).Infof(\"Error while trying to get image stream %q: %v\", name, err)\n-\t\t\t\tif errors.IsNotFound(err) {\n-\t\t\t\t\tdetails = fmt.Sprintf(\"The image trigger for image stream tag %q will have no effect because image stream %q does not exist.\", istag, name)\n+\t\t\t// Only process updates if the replica count actually changed.\n+\t\t\tif oldReplicaCount != newReplicaCount {\n+\t\t\t\tdeployment.Spec.Replicas = newReplicaCount\n+\t\t\t\t_, err := c.kubeClient.ReplicationControllers(deployment.Namespace).Update(&deployment)\n+\t\t\t\tif err != nil {\n+\t\t\t\t\tc.recorder.Eventf(config, \"DeploymentScaleFailed\", \"Failed to scale deployment %q from %d to %d: %s\", deployment.Name, oldReplicaCount, newReplicaCount, err)\n+\t\t\t\t\treturn err\n \t\t\t\t}\n+\t\t\t\tc.recorder.Eventf(config, \"DeploymentScaled\", \"Scaled deployment %q from %d to %d\", deployment.Name, oldReplicaCount, newReplicaCount)\n \t\t\t}\n \t\t}\n-\t\tallDetails = append(allDetails, details)\n-\t}\n-\n-\tif len(allDetails) > 1 {\n-\t\tfor i := range allDetails {\n-\t\t\tallDetails[i] = fmt.Sprintf(\"\\t* %s\", allDetails[i])\n-\t\t}\n-\t\t// Prepend multiple errors warning\n-\t\tmultipleErrWarning := fmt.Sprintf(\"Deployment config %q blocked by multiple errors:\\n\", config.Name)\n-\t\tallDetails = append([]string{multipleErrWarning}, allDetails...)\n+\t\treturn nil\n \t}\n-\n-\treturn strings.Join(allDetails, \"\\n\"), existingDeployments, latestDeploymentExists, nil\n-}\n-\n-// updateDetails updates a deployment configuration with the provided details\n-func (c *DeploymentConfigController) updateDetails(config *deployapi.DeploymentConfig, details string) (*deployapi.DeploymentConfig, error) {\n-\tif config.Details == nil {\n-\t\tconfig.Details = new(deployapi.DeploymentDetails)\n+\t// No deployments are running and the latest deployment doesn't exist, so\n+\t// create the new deployment.\n+\tdeployment, err := deployutil.MakeDeployment(config, c.codec)","path":"pkg/deploy/controller/deploymentconfig/controller.go","position":356,"original_position":356,"commit_id":"bb2c6ffd74b36f778f578c85b1757e7f35a267fd","original_commit_id":"c3be907b9a2f86ca41cc7867d0a03d0b533f63a9","user":{"login":"ironcladlou","id":298299,"avatar_url":"https://avatars.githubusercontent.com/u/298299?v=3","gravatar_id":"","url":"https://api.github.com/users/ironcladlou","html_url":"https://github.com/ironcladlou","followers_url":"https://api.github.com/users/ironcladlou/followers","following_url":"https://api.github.com/users/ironcladlou/following{/other_user}","gists_url":"https://api.github.com/users/ironcladlou/gists{/gist_id}","starred_url":"https://api.github.com/users/ironcladlou/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ironcladlou/subscriptions","organizations_url":"https://api.github.com/users/ironcladlou/orgs","repos_url":"https://api.github.com/users/ironcladlou/repos","events_url":"https://api.github.com/users/ironcladlou/events{/privacy}","received_events_url":"https://api.github.com/users/ironcladlou/received_events","type":"User","site_admin":false},"body":"Good catch, thanks","created_at":"2015-11-17T13:59:19Z","updated_at":"2015-11-17T13:59:19Z","html_url":"https://github.com/openshift/origin/pull/5875#discussion_r45062004","pull_request_url":"https://api.github.com/repos/openshift/origin/pulls/5875","_links":{"self":{"href":"https://api.github.com/repos/openshift/origin/pulls/comments/45062004"},"html":{"href":"https://github.com/openshift/origin/pull/5875#discussion_r45062004"},"pull_request":{"href":"https://api.github.com/repos/openshift/origin/pulls/5875"}}},"pull_request":{"url":"https://api.github.com/repos/openshift/origin/pulls/5875","id":50521607,"html_url":"https://github.com/openshift/origin/pull/5875","diff_url":"https://github.com/openshift/origin/pull/5875.diff","patch_url":"https://github.com/openshift/origin/pull/5875.patch","issue_url":"https://api.github.com/repos/openshift/origin/issues/5875","number":5875,"state":"open","locked":false,"title":"Improve deployment scaling behavior","user":{"login":"ironcladlou","id":298299,"avatar_url":"https://avatars.githubusercontent.com/u/298299?v=3","gravatar_id":"","url":"https://api.github.com/users/ironcladlou","html_url":"https://github.com/ironcladlou","followers_url":"https://api.github.com/users/ironcladlou/followers","following_url":"https://api.github.com/users/ironcladlou/following{/other_user}","gists_url":"https://api.github.com/users/ironcladlou/gists{/gist_id}","starred_url":"https://api.github.com/users/ironcladlou/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ironcladlou/subscriptions","organizations_url":"https://api.github.com/users/ironcladlou/orgs","repos_url":"https://api.github.com/users/ironcladlou/repos","events_url":"https://api.github.com/users/ironcladlou/events{/privacy}","received_events_url":"https://api.github.com/users/ironcladlou/received_events","type":"User","site_admin":false},"body":"This change makes the DeploymentConfigController solely responsible for scaling RCs owned by a deployment config which aren't currently being manipulated by a deployer process. RCs owned by the deployment config will be reconciled so that the active deployment matches the config replica count and all other RCs are scaled to 0.\r\n\r\nRationale:\r\n\r\n* Prevents races with HPA, users performing manual scaling, the deployer process, and the deployer pod controller (which was also performing scaling operations.)\r\n* Aligns replica count handling with upstream deployments; today, the replica count for deployments follows the last successful deployment and requires users to scale RCs in order to adjust subsequent deployment replica counts. Now, changes to the config replica count will drive new deployments, and scale existing deployments.\r\n* Allows all scaling operations to target the deploymentConfig only rather than RCs.\r\n\r\nThis also adds a variety of new events to provide insight into what the controller is doing.\r\n\r\nFixes #5597\r\n\r\n### TODO\r\n- [x] deployerPodController unit tests\r\n- [x] update DC scaling subresource to scale the DC directly\r\n- [x] update deployment registry to remove restriction on scaling during an in-progress deployment\r\n- [ ] e2e or extended tests to exercise scaling interactions\r\n- [ ] discussion of https://github.com/openshift/origin/pull/5875#issuecomment-156197377\r\n","created_at":"2015-11-12T15:07:36Z","updated_at":"2015-11-17T13:59:19Z","closed_at":null,"merged_at":null,"merge_commit_sha":"8ef769762b4239a3795be01ef67ecb56c2fe2f40","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/openshift/origin/pulls/5875/commits","review_comments_url":"https://api.github.com/repos/openshift/origin/pulls/5875/comments","review_comment_url":"https://api.github.com/repos/openshift/origin/pulls/comments{/number}","comments_url":"https://api.github.com/repos/openshift/origin/issues/5875/comments","statuses_url":"https://api.github.com/repos/openshift/origin/statuses/bb2c6ffd74b36f778f578c85b1757e7f35a267fd","head":{"label":"ironcladlou:dc-scaling","ref":"dc-scaling","sha":"bb2c6ffd74b36f778f578c85b1757e7f35a267fd","user":{"login":"ironcladlou","id":298299,"avatar_url":"https://avatars.githubusercontent.com/u/298299?v=3","gravatar_id":"","url":"https://api.github.com/users/ironcladlou","html_url":"https://github.com/ironcladlou","followers_url":"https://api.github.com/users/ironcladlou/followers","following_url":"https://api.github.com/users/ironcladlou/following{/other_user}","gists_url":"https://api.github.com/users/ironcladlou/gists{/gist_id}","starred_url":"https://api.github.com/users/ironcladlou/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ironcladlou/subscriptions","organizations_url":"https://api.github.com/users/ironcladlou/orgs","repos_url":"https://api.github.com/users/ironcladlou/repos","events_url":"https://api.github.com/users/ironcladlou/events{/privacy}","received_events_url":"https://api.github.com/users/ironcladlou/received_events","type":"User","site_admin":false},"repo":{"id":23721352,"name":"origin","full_name":"ironcladlou/origin","owner":{"login":"ironcladlou","id":298299,"avatar_url":"https://avatars.githubusercontent.com/u/298299?v=3","gravatar_id":"","url":"https://api.github.com/users/ironcladlou","html_url":"https://github.com/ironcladlou","followers_url":"https://api.github.com/users/ironcladlou/followers","following_url":"https://api.github.com/users/ironcladlou/following{/other_user}","gists_url":"https://api.github.com/users/ironcladlou/gists{/gist_id}","starred_url":"https://api.github.com/users/ironcladlou/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ironcladlou/subscriptions","organizations_url":"https://api.github.com/users/ironcladlou/orgs","repos_url":"https://api.github.com/users/ironcladlou/repos","events_url":"https://api.github.com/users/ironcladlou/events{/privacy}","received_events_url":"https://api.github.com/users/ironcladlou/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/ironcladlou/origin","description":"OpenShift 3 - build, deploy, and manage your applications with Docker and Kubernetes","fork":true,"url":"https://api.github.com/repos/ironcladlou/origin","forks_url":"https://api.github.com/repos/ironcladlou/origin/forks","keys_url":"https://api.github.com/repos/ironcladlou/origin/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ironcladlou/origin/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ironcladlou/origin/teams","hooks_url":"https://api.github.com/repos/ironcladlou/origin/hooks","issue_events_url":"https://api.github.com/repos/ironcladlou/origin/issues/events{/number}","events_url":"https://api.github.com/repos/ironcladlou/origin/events","assignees_url":"https://api.github.com/repos/ironcladlou/origin/assignees{/user}","branches_url":"https://api.github.com/repos/ironcladlou/origin/branches{/branch}","tags_url":"https://api.github.com/repos/ironcladlou/origin/tags","blobs_url":"https://api.github.com/repos/ironcladlou/origin/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ironcladlou/origin/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ironcladlou/origin/git/refs{/sha}","trees_url":"https://api.github.com/repos/ironcladlou/origin/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ironcladlou/origin/statuses/{sha}","languages_url":"https://api.github.com/repos/ironcladlou/origin/languages","stargazers_url":"https://api.github.com/repos/ironcladlou/origin/stargazers","contributors_url":"https://api.github.com/repos/ironcladlou/origin/contributors","subscribers_url":"https://api.github.com/repos/ironcladlou/origin/subscribers","subscription_url":"https://api.github.com/repos/ironcladlou/origin/subscription","commits_url":"https://api.github.com/repos/ironcladlou/origin/commits{/sha}","git_commits_url":"https://api.github.com/repos/ironcladlou/origin/git/commits{/sha}","comments_url":"https://api.github.com/repos/ironcladlou/origin/comments{/number}","issue_comment_url":"https://api.github.com/repos/ironcladlou/origin/issues/comments{/number}","contents_url":"https://api.github.com/repos/ironcladlou/origin/contents/{+path}","compare_url":"https://api.github.com/repos/ironcladlou/origin/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ironcladlou/origin/merges","archive_url":"https://api.github.com/repos/ironcladlou/origin/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ironcladlou/origin/downloads","issues_url":"https://api.github.com/repos/ironcladlou/origin/issues{/number}","pulls_url":"https://api.github.com/repos/ironcladlou/origin/pulls{/number}","milestones_url":"https://api.github.com/repos/ironcladlou/origin/milestones{/number}","notifications_url":"https://api.github.com/repos/ironcladlou/origin/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ironcladlou/origin/labels{/name}","releases_url":"https://api.github.com/repos/ironcladlou/origin/releases{/id}","created_at":"2014-09-05T23:22:09Z","updated_at":"2015-04-29T15:08:27Z","pushed_at":"2015-11-17T13:59:11Z","git_url":"git://github.com/ironcladlou/origin.git","ssh_url":"git@github.com:ironcladlou/origin.git","clone_url":"https://github.com/ironcladlou/origin.git","svn_url":"https://github.com/ironcladlou/origin","homepage":"https://openshift.github.io","size":179662,"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":"08471b135e168e77de716fee9e888fe78cf5f249","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-11-17T13:43:45Z","pushed_at":"2015-11-17T13:59:11Z","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":225992,"stargazers_count":810,"watchers_count":810,"language":"Go","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":503,"mirror_url":null,"open_issues_count":669,"forks":503,"open_issues":669,"watchers":810,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/openshift/origin/pulls/5875"},"html":{"href":"https://github.com/openshift/origin/pull/5875"},"issue":{"href":"https://api.github.com/repos/openshift/origin/issues/5875"},"comments":{"href":"https://api.github.com/repos/openshift/origin/issues/5875/comments"},"review_comments":{"href":"https://api.github.com/repos/openshift/origin/pulls/5875/comments"},"review_comment":{"href":"https://api.github.com/repos/openshift/origin/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/openshift/origin/pulls/5875/commits"},"statuses":{"href":"https://api.github.com/repos/openshift/origin/statuses/bb2c6ffd74b36f778f578c85b1757e7f35a267fd"}}}}