// findTargetDeployment finds the deployment which is the rollback target by // searching for deployments associated with config. If desiredVersion is >0, // the deployment matching desiredVersion will be returned. If desiredVersion // is <=0, the last completed deployment which is older than the config's // version will be returned. func (o *RollbackOptions) findTargetDeployment(config *deployapi.DeploymentConfig, desiredVersion int64) (*kapi.ReplicationController, error) { // Find deployments for the config sorted by version descending. deployments, err := o.kc.ReplicationControllers(config.Namespace).List(kapi.ListOptions{LabelSelector: deployutil.ConfigSelector(config.Name)}) if err != nil { return nil, err } sort.Sort(deployutil.ByLatestVersionDesc(deployments.Items)) // Find the target deployment for rollback. If a version was specified, // use the version for a search. Otherwise, use the last completed // deployment. var target *kapi.ReplicationController for _, deployment := range deployments.Items { version := deployutil.DeploymentVersionFor(&deployment) if desiredVersion > 0 { if version == desiredVersion { target = &deployment break } } else { if version < config.Status.LatestVersion && deployutil.DeploymentStatusFor(&deployment) == deployapi.DeploymentStatusComplete { target = &deployment break } } } if target == nil { return nil, fmt.Errorf("couldn't find deployment for rollback") } return target, nil }
// cancel cancels any deployment process in progress for config. func (o DeployOptions) cancel(config *deployapi.DeploymentConfig) error { if config.Spec.Paused { return fmt.Errorf("cannot cancel a paused deployment config") } deployments, err := o.kubeClient.ReplicationControllers(config.Namespace).List(kapi.ListOptions{LabelSelector: deployutil.ConfigSelector(config.Name)}) if err != nil { return err } if len(deployments.Items) == 0 { fmt.Fprintf(o.out, "There have been no deployments for %s/%s\n", config.Namespace, config.Name) return nil } sort.Sort(deployutil.ByLatestVersionDesc(deployments.Items)) failedCancellations := []string{} anyCancelled := false for _, deployment := range deployments.Items { status := deployutil.DeploymentStatusFor(&deployment) switch status { case deployapi.DeploymentStatusNew, deployapi.DeploymentStatusPending, deployapi.DeploymentStatusRunning: if deployutil.IsDeploymentCancelled(&deployment) { continue } deployment.Annotations[deployapi.DeploymentCancelledAnnotation] = deployapi.DeploymentCancelledAnnotationValue deployment.Annotations[deployapi.DeploymentStatusReasonAnnotation] = deployapi.DeploymentCancelledByUser _, err := o.kubeClient.ReplicationControllers(deployment.Namespace).Update(&deployment) if err == nil { fmt.Fprintf(o.out, "Cancelled deployment #%d\n", config.Status.LatestVersion) anyCancelled = true } else { fmt.Fprintf(o.out, "Couldn't cancel deployment #%d (status: %s): %v\n", deployutil.DeploymentVersionFor(&deployment), status, err) failedCancellations = append(failedCancellations, strconv.FormatInt(deployutil.DeploymentVersionFor(&deployment), 10)) } } } if len(failedCancellations) > 0 { return fmt.Errorf("couldn't cancel deployment %s", strings.Join(failedCancellations, ", ")) } if !anyCancelled { latest := &deployments.Items[0] maybeCancelling := "" if deployutil.IsDeploymentCancelled(latest) && !deployutil.IsTerminatedDeployment(latest) { maybeCancelling = " (cancelling)" } timeAt := strings.ToLower(units.HumanDuration(time.Now().Sub(latest.CreationTimestamp.Time))) fmt.Fprintf(o.out, "No deployments are in progress (latest deployment #%d %s%s %s ago)\n", deployutil.DeploymentVersionFor(latest), strings.ToLower(string(deployutil.DeploymentStatusFor(latest))), maybeCancelling, timeAt) } return nil }
func (o CancelOptions) forEachControllerInConfig(namespace, name string, mutateFunc func(*kapi.ReplicationController) bool) ([]kapi.ReplicationController, bool, error) { deployments, err := o.Clientset.ReplicationControllers(namespace).List(kapi.ListOptions{LabelSelector: deployutil.ConfigSelector(name)}) if err != nil { return nil, false, err } if len(deployments.Items) == 0 { return nil, false, fmt.Errorf("there have been no replication controllers for %s/%s\n", namespace, name) } sort.Sort(deployutil.ByLatestVersionDesc(deployments.Items)) allErrs := []error{} cancelled := false for _, deployment := range deployments.Items { status := deployutil.DeploymentStatusFor(&deployment) switch status { case deployapi.DeploymentStatusNew, deployapi.DeploymentStatusPending, deployapi.DeploymentStatusRunning: cancelled = mutateFunc(&deployment) } } return deployments.Items, cancelled, utilerrors.NewAggregate(allErrs) }
// Deploy starts the deployment process for deploymentName. func (d *Deployer) Deploy(namespace, deploymentName string) error { // Look up the new deployment. to, err := d.getDeployment(namespace, deploymentName) if err != nil { return fmt.Errorf("couldn't get deployment %s/%s: %v", namespace, deploymentName, err) } // Decode the config from the deployment. config, err := deployutil.DecodeDeploymentConfig(to, latest.Codec) if err != nil { return fmt.Errorf("couldn't decode deployment config from deployment %s/%s: %v", to.Namespace, to.Name, err) } // Get a strategy for the deployment. strategy, err := d.strategyFor(config) if err != nil { return err } // New deployments must have a desired replica count. desiredReplicas, hasDesired := deployutil.DeploymentDesiredReplicas(to) if !hasDesired { return fmt.Errorf("deployment %s has no desired replica count", deployutil.LabelForDeployment(to)) } // Find all deployments for the config. unsortedDeployments, err := d.getDeployments(namespace, config.Name) if err != nil { return fmt.Errorf("couldn't get controllers in namespace %s: %v", namespace, err) } deployments := unsortedDeployments.Items // Sort all the deployments by version. sort.Sort(deployutil.ByLatestVersionDesc(deployments)) // Find any last completed deployment. var from *kapi.ReplicationController for _, candidate := range deployments { if candidate.Name == to.Name { continue } if deployutil.DeploymentStatusFor(&candidate) == deployapi.DeploymentStatusComplete { from = &candidate break } } // Scale down any deployments which aren't the new or last deployment. for _, candidate := range deployments { // Skip the from/to deployments. if candidate.Name == to.Name { continue } if from != nil && candidate.Name == from.Name { continue } // Skip the deployment if it's already scaled down. if candidate.Spec.Replicas == 0 { continue } // Scale the deployment down to zero. retryWaitParams := kubectl.NewRetryParams(1*time.Second, 120*time.Second) if err := d.scaler.Scale(candidate.Namespace, candidate.Name, uint(0), &kubectl.ScalePrecondition{Size: -1, ResourceVersion: ""}, retryWaitParams, retryWaitParams); err != nil { glog.Errorf("Couldn't scale down prior deployment %s: %v", deployutil.LabelForDeployment(&candidate), err) } else { glog.Infof("Scaled down prior deployment %s", deployutil.LabelForDeployment(&candidate)) } } // Perform the deployment. if from == nil { glog.Infof("Deploying %s for the first time (replicas: %d)", deployutil.LabelForDeployment(to), desiredReplicas) } else { glog.Infof("Deploying from %s to %s (replicas: %d)", deployutil.LabelForDeployment(from), deployutil.LabelForDeployment(to), desiredReplicas) } return strategy.Deploy(from, to, desiredReplicas) }
func TestHandleScenarios(t *testing.T) { type deployment struct { // version is the deployment version version int // replicas is the spec replicas of the deployment replicas int // replicasA is the annotated replica value for backwards compat checks replicasA *int desiredA *int status deployapi.DeploymentStatus cancelled bool } mkdeployment := func(d deployment) kapi.ReplicationController { deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(d.version), kapi.Codec) deployment.Annotations[deployapi.DeploymentStatusAnnotation] = string(d.status) if d.cancelled { deployment.Annotations[deployapi.DeploymentCancelledAnnotation] = deployapi.DeploymentCancelledAnnotationValue deployment.Annotations[deployapi.DeploymentStatusReasonAnnotation] = deployapi.DeploymentCancelledNewerDeploymentExists } if d.replicasA != nil { deployment.Annotations[deployapi.DeploymentReplicasAnnotation] = strconv.Itoa(*d.replicasA) } else { delete(deployment.Annotations, deployapi.DeploymentReplicasAnnotation) } if d.desiredA != nil { deployment.Annotations[deployapi.DesiredReplicasAnnotation] = strconv.Itoa(*d.desiredA) } else { delete(deployment.Annotations, deployapi.DesiredReplicasAnnotation) } deployment.Spec.Replicas = d.replicas return *deployment } tests := []struct { name string // replicas is the config replicas prior to the update replicas int // newVersion is the version of the config at the time of the update newVersion int // expectedReplicas is the expected config replica count after the update expectedReplicas int // before is the state of all deployments prior to the update before []deployment // after is the expected state of all deployments after the update after []deployment // errExpected is whether the update should produce an error errExpected bool }{ { name: "version is zero", replicas: 1, newVersion: 0, expectedReplicas: 1, before: []deployment{}, after: []deployment{}, errExpected: false, }, { name: "first deployment", replicas: 1, newVersion: 1, expectedReplicas: 1, before: []deployment{}, after: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, errExpected: false, }, { name: "initial deployment already in progress", replicas: 1, newVersion: 1, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, errExpected: false, }, { name: "new version", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, errExpected: false, }, { name: "already in progress", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, errExpected: false, }, { name: "already deployed", replicas: 1, newVersion: 1, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, { name: "awaiting cancellation of older deployments", replicas: 1, newVersion: 3, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), desiredA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusRunning, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), desiredA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusRunning, cancelled: true}, }, errExpected: true, }, { name: "awaiting cancellation of older deployments (already cancelled)", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusRunning, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusRunning, cancelled: true}, }, errExpected: true, }, { name: "steady state replica corrections (latest == active)", replicas: 1, newVersion: 5, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 1, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 5, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 5, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, { name: "steady state replica corrections (latest != active)", replicas: 1, newVersion: 5, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 1, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 5, replicas: 1, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 5, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, errExpected: false, }, { name: "already deployed, no active deployment", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, errExpected: false, }, { name: "scale up latest/active completed deployment", replicas: 5, newVersion: 2, expectedReplicas: 5, before: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 5, replicasA: newint(5), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, { name: "scale up active (not latest) completed deployment", replicas: 5, newVersion: 2, expectedReplicas: 5, before: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 5, replicasA: newint(5), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, { name: "scale down latest/active completed deployment", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 5, replicasA: newint(5), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, { name: "scale down active (not latest) completed deployment", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 5, replicasA: newint(5), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, { name: "fallback to last completed deployment", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, { name: "fallback to last completed deployment (partial rollout)", replicas: 5, newVersion: 2, expectedReplicas: 5, before: []deployment{ {version: 1, replicas: 2, replicasA: newint(5), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 2, replicasA: newint(0), desiredA: newint(5), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 5, replicasA: newint(5), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(5), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, // The cases below will exercise backwards compatibility for resources // which predate the use of the replica annotation. { name: "(compat) initial deployment already in progress", replicas: 1, newVersion: 1, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, desiredA: newint(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, desiredA: newint(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, errExpected: false, }, { name: "(compat) new version", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, errExpected: false, }, { name: "(compat) already in progress", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, desiredA: newint(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, desiredA: newint(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, errExpected: false, }, { name: "(compat) already deployed", replicas: 1, newVersion: 1, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, { name: "(compat) awaiting cancellation of older deployments", replicas: 1, newVersion: 3, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, desiredA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, desiredA: newint(1), status: deployapi.DeploymentStatusRunning, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, desiredA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, desiredA: newint(1), status: deployapi.DeploymentStatusRunning, cancelled: true}, }, errExpected: true, }, { name: "(compat) awaiting cancellation of older deployments (already cancelled)", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, desiredA: newint(1), status: deployapi.DeploymentStatusRunning, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 1, desiredA: newint(1), status: deployapi.DeploymentStatusRunning, cancelled: true}, }, errExpected: true, }, { name: "(compat) steady state replica corrections (latest == active)", replicas: 5, newVersion: 5, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 1, desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 0, desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 5, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 5, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, { name: "(compat) steady state replica corrections (latest != active)", replicas: 5, newVersion: 5, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 1, desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 0, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 5, replicas: 0, desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 5, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, errExpected: false, }, { name: "(compat) already deployed, no active deployment", replicas: 2, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 2, replicas: 0, desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, errExpected: false, }, { name: "(compat) scale up latest/active completed deployment", replicas: 1, newVersion: 2, expectedReplicas: 5, before: []deployment{ {version: 1, replicas: 0, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 5, status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 5, replicasA: newint(5), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, // No longer supported. { name: "(compat) scale up active (not latest) completed deployment (RC targetted directly)", replicas: 2, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 5, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, { name: "(compat) scale up active (not latest) completed deployment (RC targetted via oc)", replicas: 1, newVersion: 2, expectedReplicas: 2, before: []deployment{ {version: 1, replicas: 2, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 5, desiredA: newint(2), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 2, replicasA: newint(2), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(2), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, { name: "(compat) fallback to last completed deployment", replicas: 3, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, // The cases below exercise old clients performing scaling operations // against new resources and controller behavior. { name: "(compat-2) scale up latest/active completed deployment (via oc)", replicas: 1, newVersion: 2, expectedReplicas: 2, before: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 2, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newint(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 2, replicasA: newint(2), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, { name: "(compat-2) scale up active (not latest) completed deployment (via oc)", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 2, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newint(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newint(0), desiredA: newint(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, } for _, test := range tests { t.Logf("evaluating test: %s", test.name) deployments := map[string]kapi.ReplicationController{} for _, template := range test.before { deployment := mkdeployment(template) deployments[deployment.Name] = deployment } kc := &ktestclient.Fake{} kc.AddReactor("list", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) { list := []kapi.ReplicationController{} for _, deployment := range deployments { list = append(list, deployment) } return true, &kapi.ReplicationControllerList{Items: list}, nil }) kc.AddReactor("create", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) { rc := action.(ktestclient.CreateAction).GetObject().(*kapi.ReplicationController) deployments[rc.Name] = *rc return true, rc, nil }) kc.AddReactor("update", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) { rc := action.(ktestclient.UpdateAction).GetObject().(*kapi.ReplicationController) deployments[rc.Name] = *rc return true, rc, nil }) oc := &testclient.Fake{} recorder := &record.FakeRecorder{} controller := &DeploymentConfigController{ kubeClient: kc, osClient: oc, codec: kapi.Codec, recorder: recorder, } config := deploytest.OkDeploymentConfig(test.newVersion) config.Spec.Replicas = test.replicas err := controller.Handle(config) if err != nil && !test.errExpected { t.Fatalf("unexpected error: %s", err) } expectedDeployments := []kapi.ReplicationController{} for _, template := range test.after { expectedDeployments = append(expectedDeployments, mkdeployment(template)) } actualDeployments := []kapi.ReplicationController{} for _, deployment := range deployments { actualDeployments = append(actualDeployments, deployment) } sort.Sort(deployutil.ByLatestVersionDesc(expectedDeployments)) sort.Sort(deployutil.ByLatestVersionDesc(actualDeployments)) if e, a := test.expectedReplicas, config.Spec.Replicas; e != a { t.Errorf("expected config replicas to be %d, got %d", e, a) t.Fatalf("events:\n%s", strings.Join(recorder.Events, "\t\n")) } anyDeploymentMismatches := false for i := 0; i < len(expectedDeployments); i++ { expected, actual := expectedDeployments[i], actualDeployments[i] if !kapi.Semantic.DeepEqual(expected, actual) { anyDeploymentMismatches = true t.Errorf("actual deployment don't match expected: %v", kutil.ObjectDiff(expected, actual)) } } if anyDeploymentMismatches { t.Fatalf("events:\n%s", strings.Join(recorder.Events, "\t\n")) } } }
// Handle processes config and creates a new deployment if necessary. func (c *DeploymentConfigController) Handle(config *deployapi.DeploymentConfig) error { // Inspect a deployment configuration every time the controller reconciles it details, existingDeployments, latestDeploymentExists, err := c.findDetails(config) if err != nil { return err } config, err = c.updateDetails(config, details) if err != nil { return transientError(err.Error()) } // Only deploy when the version has advanced past 0. if config.LatestVersion == 0 { glog.V(5).Infof("Waiting for first version of %s", deployutil.LabelForDeploymentConfig(config)) return nil } var inflightDeployment *kapi.ReplicationController for _, deployment := range existingDeployments.Items { deploymentStatus := deployutil.DeploymentStatusFor(&deployment) switch deploymentStatus { case deployapi.DeploymentStatusFailed, deployapi.DeploymentStatusComplete: // Previous deployment in terminal state - can ignore // Ignoring specific deployment states so that any newly introduced // deployment state will not be ignored default: if inflightDeployment == nil { inflightDeployment = &deployment continue } var deploymentForCancellation *kapi.ReplicationController if deployutil.DeploymentVersionFor(inflightDeployment) < deployutil.DeploymentVersionFor(&deployment) { deploymentForCancellation, inflightDeployment = inflightDeployment, &deployment } else { deploymentForCancellation = &deployment } deploymentForCancellation.Annotations[deployapi.DeploymentCancelledAnnotation] = deployapi.DeploymentCancelledAnnotationValue deploymentForCancellation.Annotations[deployapi.DeploymentStatusReasonAnnotation] = deployapi.DeploymentCancelledNewerDeploymentExists if _, err := c.deploymentClient.updateDeployment(deploymentForCancellation.Namespace, deploymentForCancellation); err != nil { util.HandleError(fmt.Errorf("couldn't cancel deployment %s: %v", deployutil.LabelForDeployment(deploymentForCancellation), err)) } glog.V(4).Infof("Cancelled deployment %s for deployment config %s", deployutil.LabelForDeployment(deploymentForCancellation), deployutil.LabelForDeploymentConfig(config)) } } // if the latest deployment exists then nothing else needs to be done if latestDeploymentExists { return nil } // check to see if there are inflight deployments if inflightDeployment != nil { // raise a transientError so that the deployment config can be re-queued glog.V(4).Infof("Found previous inflight deployment for %s - will requeue", deployutil.LabelForDeploymentConfig(config)) return transientError(fmt.Sprintf("found previous inflight deployment for %s - requeuing", deployutil.LabelForDeploymentConfig(config))) } // Try and build a deployment for the config. deployment, err := c.makeDeployment(config) if err != nil { return fatalError(fmt.Sprintf("couldn't make deployment from (potentially invalid) deployment config %s: %v", deployutil.LabelForDeploymentConfig(config), err)) } // Compute the desired replicas for the deployment. Use the last completed // deployment's current replica count, or the config template if there is no // prior completed deployment available. desiredReplicas := config.Template.ControllerTemplate.Replicas if len(existingDeployments.Items) > 0 { sort.Sort(deployutil.ByLatestVersionDesc(existingDeployments.Items)) for _, existing := range existingDeployments.Items { if deployutil.DeploymentStatusFor(&existing) == deployapi.DeploymentStatusComplete { desiredReplicas = existing.Spec.Replicas glog.V(4).Infof("Desired replicas for %s set to %d based on prior completed deployment %s", deployutil.LabelForDeploymentConfig(config), desiredReplicas, existing.Name) break } } } deployment.Annotations[deployapi.DesiredReplicasAnnotation] = strconv.Itoa(desiredReplicas) // Create the deployment. if _, err := c.deploymentClient.createDeployment(config.Namespace, deployment); err == nil { glog.V(4).Infof("Created deployment for deployment config %s", deployutil.LabelForDeploymentConfig(config)) return nil } else { // If the deployment was already created, just move on. The cache could be stale, or another // process could have already handled this update. if errors.IsAlreadyExists(err) { glog.V(4).Infof("Deployment already exists for deployment config %s", deployutil.LabelForDeploymentConfig(config)) return nil } glog.Warningf("Cannot create latest deployment for deployment config %q: %v", deployutil.LabelForDeploymentConfig(config), err) return fmt.Errorf("couldn't create deployment for deployment config %s: %v", deployutil.LabelForDeploymentConfig(config), err) } }
func TestHandleScenarios(t *testing.T) { type deployment struct { // version is the deployment version version int64 // replicas is the spec replicas of the deployment replicas int32 // test is whether this is a test deployment config test bool // replicasA is the annotated replica value for backwards compat checks replicasA *int32 desiredA *int32 status deployapi.DeploymentStatus cancelled bool } mkdeployment := func(d deployment) kapi.ReplicationController { config := deploytest.OkDeploymentConfig(d.version) if d.test { config = deploytest.TestDeploymentConfig(config) } deployment, _ := deployutil.MakeDeployment(config, kapi.Codecs.LegacyCodec(deployapi.SchemeGroupVersion)) deployment.Annotations[deployapi.DeploymentStatusAnnotation] = string(d.status) if d.cancelled { deployment.Annotations[deployapi.DeploymentCancelledAnnotation] = deployapi.DeploymentCancelledAnnotationValue deployment.Annotations[deployapi.DeploymentStatusReasonAnnotation] = deployapi.DeploymentCancelledNewerDeploymentExists } if d.replicasA != nil { deployment.Annotations[deployapi.DeploymentReplicasAnnotation] = strconv.Itoa(int(*d.replicasA)) } else { delete(deployment.Annotations, deployapi.DeploymentReplicasAnnotation) } if d.desiredA != nil { deployment.Annotations[deployapi.DesiredReplicasAnnotation] = strconv.Itoa(int(*d.desiredA)) } else { delete(deployment.Annotations, deployapi.DesiredReplicasAnnotation) } deployment.Spec.Replicas = d.replicas return *deployment } tests := []struct { name string // replicas is the config replicas prior to the update replicas int32 // test is whether this is a test deployment config test bool // newVersion is the version of the config at the time of the update newVersion int64 // expectedReplicas is the expected config replica count after the update expectedReplicas int32 // before is the state of all deployments prior to the update before []deployment // after is the expected state of all deployments after the update after []deployment // errExpected is whether the update should produce an error errExpected bool }{ { name: "version is zero", replicas: 1, newVersion: 0, expectedReplicas: 1, before: []deployment{}, after: []deployment{}, errExpected: false, }, { name: "first deployment", replicas: 1, newVersion: 1, expectedReplicas: 1, before: []deployment{}, after: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, errExpected: false, }, { name: "initial deployment already in progress", replicas: 1, newVersion: 1, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, errExpected: false, }, { name: "new version", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, errExpected: false, }, { name: "already in progress", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, errExpected: false, }, { name: "already deployed", replicas: 1, newVersion: 1, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, { name: "awaiting cancellation of older deployments", replicas: 1, newVersion: 3, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), desiredA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusRunning, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), desiredA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusRunning, cancelled: true}, }, errExpected: true, }, { name: "awaiting cancellation of older deployments (already cancelled)", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusRunning, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusRunning, cancelled: true}, }, errExpected: true, }, { name: "steady state replica corrections (latest == active)", replicas: 1, newVersion: 5, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 1, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 5, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 5, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, { name: "steady state replica corrections (latest != active)", replicas: 1, newVersion: 5, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 1, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 5, replicas: 1, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 5, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, errExpected: false, }, { name: "already deployed, no active deployment", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, errExpected: false, }, { name: "scale up latest/active completed deployment", replicas: 5, newVersion: 2, expectedReplicas: 5, before: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 5, replicasA: newInt32(5), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, { name: "scale up active (not latest) completed deployment", replicas: 5, newVersion: 2, expectedReplicas: 5, before: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 5, replicasA: newInt32(5), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, { name: "scale down latest/active completed deployment", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 5, replicasA: newInt32(5), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, { name: "scale down active (not latest) completed deployment", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 5, replicasA: newInt32(5), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, { name: "fallback to last completed deployment", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, { name: "fallback to last completed deployment (partial rollout)", replicas: 5, newVersion: 2, expectedReplicas: 5, before: []deployment{ {version: 1, replicas: 2, replicasA: newInt32(5), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 2, replicasA: newInt32(0), desiredA: newInt32(5), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 5, replicasA: newInt32(5), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(5), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, // The cases below will exercise backwards compatibility for resources // which predate the use of the replica annotation. { name: "(compat) initial deployment already in progress", replicas: 1, newVersion: 1, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, desiredA: newInt32(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, desiredA: newInt32(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, errExpected: false, }, { name: "(compat) new version", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, errExpected: false, }, { name: "(compat) already in progress", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, desiredA: newInt32(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, desiredA: newInt32(1), status: deployapi.DeploymentStatusNew, cancelled: false}, }, errExpected: false, }, { name: "(compat) already deployed", replicas: 1, newVersion: 1, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, { name: "(compat) awaiting cancellation of older deployments", replicas: 1, newVersion: 3, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, desiredA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, desiredA: newInt32(1), status: deployapi.DeploymentStatusRunning, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 1, desiredA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, desiredA: newInt32(1), status: deployapi.DeploymentStatusRunning, cancelled: true}, }, errExpected: true, }, { name: "(compat) awaiting cancellation of older deployments (already cancelled)", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, desiredA: newInt32(1), status: deployapi.DeploymentStatusRunning, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 1, desiredA: newInt32(1), status: deployapi.DeploymentStatusRunning, cancelled: true}, }, errExpected: true, }, { name: "(compat) steady state replica corrections (latest == active)", replicas: 5, newVersion: 5, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 1, desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 0, desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 5, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 5, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, { name: "(compat) steady state replica corrections of a test config (latest == active)", test: true, replicas: 5, newVersion: 5, expectedReplicas: 5, before: []deployment{ {version: 1, replicas: 0, status: deployapi.DeploymentStatusComplete, cancelled: false, test: true}, {version: 2, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false, test: true}, {version: 3, replicas: 1, desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true, test: true}, {version: 4, replicas: 0, desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false, test: true}, {version: 5, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false, test: true}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false, test: true}, {version: 2, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false, test: true}, {version: 3, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true, test: true}, {version: 4, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false, test: true}, {version: 5, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false, test: true}, }, errExpected: false, }, { name: "(compat) steady state replica corrections (latest != active)", replicas: 5, newVersion: 5, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 1, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 1, desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 0, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 5, replicas: 0, desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 3, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, {version: 4, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 5, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, errExpected: false, }, { name: "(compat) already deployed, no active deployment", replicas: 2, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 2, replicas: 0, desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: false}, }, errExpected: false, }, { name: "(compat) scale up latest/active completed deployment", replicas: 1, newVersion: 2, expectedReplicas: 5, before: []deployment{ {version: 1, replicas: 0, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 5, status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 5, replicasA: newInt32(5), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, { name: "(compat) scale up latest/active completed test deployment", test: true, replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, status: deployapi.DeploymentStatusComplete, cancelled: false, test: true}, {version: 2, replicas: 5, status: deployapi.DeploymentStatusComplete, cancelled: false, test: true}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false, test: true}, {version: 2, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false, test: true}, }, errExpected: false, }, { name: "(compat) scale up latest/active running test deployment", test: true, replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, status: deployapi.DeploymentStatusComplete, cancelled: false, test: true}, {version: 2, replicas: 5, status: deployapi.DeploymentStatusRunning, cancelled: false, test: true}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: nil, status: deployapi.DeploymentStatusComplete, cancelled: false, test: true}, {version: 2, replicas: 5, replicasA: nil, status: deployapi.DeploymentStatusRunning, cancelled: false, test: true}, }, errExpected: false, }, // No longer supported. { name: "(compat) scale up active (not latest) completed deployment (RC targetted directly)", replicas: 2, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 5, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, { name: "(compat) scale up active (not latest) completed deployment (RC targetted via oc)", replicas: 1, newVersion: 2, expectedReplicas: 2, before: []deployment{ {version: 1, replicas: 2, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 5, desiredA: newInt32(2), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 2, replicasA: newInt32(2), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(2), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, { name: "(compat) fallback to last completed deployment", replicas: 3, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 0, status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, // The cases below exercise old clients performing scaling operations // against new resources and controller behavior. { name: "(compat-2) scale up latest/active completed deployment (via oc)", replicas: 1, newVersion: 2, expectedReplicas: 2, before: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 2, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, after: []deployment{ {version: 1, replicas: 0, replicasA: newInt32(0), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 2, replicasA: newInt32(2), status: deployapi.DeploymentStatusComplete, cancelled: false}, }, errExpected: false, }, { name: "(compat-2) scale up active (not latest) completed deployment (via oc)", replicas: 1, newVersion: 2, expectedReplicas: 1, before: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 2, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, after: []deployment{ {version: 1, replicas: 1, replicasA: newInt32(1), status: deployapi.DeploymentStatusComplete, cancelled: false}, {version: 2, replicas: 0, replicasA: newInt32(0), desiredA: newInt32(1), status: deployapi.DeploymentStatusFailed, cancelled: true}, }, errExpected: false, }, } for _, test := range tests { t.Logf("evaluating test: %s", test.name) deployments := map[string]kapi.ReplicationController{} toStore := []kapi.ReplicationController{} for _, template := range test.before { deployment := mkdeployment(template) deployments[deployment.Name] = deployment toStore = append(toStore, deployment) } oc := &testclient.Fake{} kc := &ktestclient.Fake{} kc.AddReactor("create", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) { rc := action.(ktestclient.CreateAction).GetObject().(*kapi.ReplicationController) deployments[rc.Name] = *rc return true, rc, nil }) kc.AddReactor("update", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) { rc := action.(ktestclient.UpdateAction).GetObject().(*kapi.ReplicationController) deployments[rc.Name] = *rc return true, rc, nil }) codec := kapi.Codecs.LegacyCodec(deployapi.SchemeGroupVersion) dcInformer := framework.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options kapi.ListOptions) (runtime.Object, error) { return oc.DeploymentConfigs(kapi.NamespaceAll).List(options) }, WatchFunc: func(options kapi.ListOptions) (watch.Interface, error) { return oc.DeploymentConfigs(kapi.NamespaceAll).Watch(options) }, }, &deployapi.DeploymentConfig{}, 2*time.Minute, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, ) rcInformer := framework.NewSharedIndexInformer( &cache.ListWatch{ ListFunc: func(options kapi.ListOptions) (runtime.Object, error) { return kc.ReplicationControllers(kapi.NamespaceAll).List(options) }, WatchFunc: func(options kapi.ListOptions) (watch.Interface, error) { return kc.ReplicationControllers(kapi.NamespaceAll).Watch(options) }, }, &kapi.ReplicationController{}, 2*time.Minute, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, ) c := NewDeploymentConfigController(dcInformer, rcInformer, oc, kc, codec) for i := range toStore { c.rcStore.Add(&toStore[i]) } config := deploytest.OkDeploymentConfig(test.newVersion) if test.test { config = deploytest.TestDeploymentConfig(config) } config.Spec.Replicas = test.replicas if err := c.Handle(config); err != nil && !test.errExpected { t.Errorf("unexpected error: %s", err) continue } expectedDeployments := []kapi.ReplicationController{} for _, template := range test.after { expectedDeployments = append(expectedDeployments, mkdeployment(template)) } actualDeployments := []kapi.ReplicationController{} for _, deployment := range deployments { actualDeployments = append(actualDeployments, deployment) } sort.Sort(deployutil.ByLatestVersionDesc(expectedDeployments)) sort.Sort(deployutil.ByLatestVersionDesc(actualDeployments)) if e, a := test.expectedReplicas, config.Spec.Replicas; e != a { t.Errorf("expected config replicas to be %d, got %d", e, a) continue } for i := 0; i < len(expectedDeployments); i++ { expected, actual := expectedDeployments[i], actualDeployments[i] if !kapi.Semantic.DeepEqual(expected, actual) { t.Errorf("actual deployment don't match expected: %v", diff.ObjectDiff(expected, actual)) } } } }
// Deploy starts the deployment process for rcName. func (d *Deployer) Deploy(namespace, rcName string) error { // Look up the new deployment. to, err := d.getDeployment(namespace, rcName) if err != nil { return fmt.Errorf("couldn't get deployment %s: %v", rcName, err) } // Decode the config from the deployment. config, err := deployutil.DecodeDeploymentConfig(to, kapi.Codecs.UniversalDecoder()) if err != nil { return fmt.Errorf("couldn't decode deployment config from deployment %s: %v", to.Name, err) } // Get a strategy for the deployment. s, err := d.strategyFor(config) if err != nil { return err } // New deployments must have a desired replica count. desiredReplicas, hasDesired := deployutil.DeploymentDesiredReplicas(to) if !hasDesired { return fmt.Errorf("deployment %s has already run to completion", to.Name) } // Find all deployments for the config. unsortedDeployments, err := d.getDeployments(namespace, config.Name) if err != nil { return fmt.Errorf("couldn't get controllers in namespace %s: %v", namespace, err) } deployments := unsortedDeployments.Items // Sort all the deployments by version. sort.Sort(deployutil.ByLatestVersionDesc(deployments)) // Find any last completed deployment. var from *kapi.ReplicationController for _, candidate := range deployments { if candidate.Name == to.Name { continue } if deployutil.IsCompleteDeployment(&candidate) { from = &candidate break } } if deployutil.DeploymentVersionFor(to) < deployutil.DeploymentVersionFor(from) { return fmt.Errorf("deployment %s is older than %s", to.Name, from.Name) } // Scale down any deployments which aren't the new or last deployment. for _, candidate := range deployments { // Skip the from/to deployments. if candidate.Name == to.Name { continue } if from != nil && candidate.Name == from.Name { continue } // Skip the deployment if it's already scaled down. if candidate.Spec.Replicas == 0 { continue } // Scale the deployment down to zero. retryWaitParams := kubectl.NewRetryParams(1*time.Second, 120*time.Second) if err := d.scaler.Scale(candidate.Namespace, candidate.Name, uint(0), &kubectl.ScalePrecondition{Size: -1, ResourceVersion: ""}, retryWaitParams, retryWaitParams); err != nil { fmt.Fprintf(d.errOut, "error: Couldn't scale down prior deployment %s: %v\n", deployutil.LabelForDeployment(&candidate), err) } else { fmt.Fprintf(d.out, "--> Scaled older deployment %s down\n", candidate.Name) } } if d.until == "start" { return strategy.NewConditionReachedErr("Ready to start deployment") } // Perform the deployment. if err := s.Deploy(from, to, int(desiredReplicas)); err != nil { return err } fmt.Fprintf(d.out, "--> Success\n") return nil }
func (c *DeployerPodController) cleanupFailedDeployment(deployment *kapi.ReplicationController) error { // Scale down the current failed deployment configName := deployutil.DeploymentConfigNameFor(deployment) existingDeployments, err := c.deploymentClient.listDeploymentsForConfig(deployment.Namespace, configName) if err != nil { return fmt.Errorf("couldn't list Deployments for DeploymentConfig %s: %v", configName, err) } desiredReplicas, ok := deployutil.DeploymentDesiredReplicas(deployment) if !ok { // if desired replicas could not be found, then log the error // and update the failed deployment // this cannot be treated as a transient error kutil.HandleError(fmt.Errorf("Could not determine desired replicas from %s to reset replicas for last completed deployment", deployutil.LabelForDeployment(deployment))) } if ok && len(existingDeployments.Items) > 0 { sort.Sort(deployutil.ByLatestVersionDesc(existingDeployments.Items)) for index, existing := range existingDeployments.Items { // if a newer deployment exists: // - set the replicas for the current failed deployment to 0 // - there is no point in scaling up the last completed deployment // since that will be scaled down by the later deployment if index == 0 && existing.Name != deployment.Name { break } // the latest completed deployment is the one that needs to be scaled back up if deployutil.DeploymentStatusFor(&existing) == deployapi.DeploymentStatusComplete { if existing.Spec.Replicas == desiredReplicas { break } // scale back the completed deployment to the target of the failed deployment existing.Spec.Replicas = desiredReplicas if _, err := c.deploymentClient.updateDeployment(existing.Namespace, &existing); err != nil { if kerrors.IsNotFound(err) { return nil } return fmt.Errorf("couldn't update replicas to %d for deployment %s: %v", desiredReplicas, deployutil.LabelForDeployment(&existing), err) } glog.V(4).Infof("Updated replicas to %d for deployment %s", desiredReplicas, deployutil.LabelForDeployment(&existing)) break } } } // set the replicas for the failed deployment to 0 // and set the status to Failed deployment.Spec.Replicas = 0 deployment.Annotations[deployapi.DeploymentStatusAnnotation] = string(deployapi.DeploymentStatusFailed) if _, err := c.deploymentClient.updateDeployment(deployment.Namespace, deployment); err != nil { if kerrors.IsNotFound(err) { return nil } return fmt.Errorf("couldn't scale down the deployment %s and mark it as failed: %v", deployutil.LabelForDeployment(deployment), err) } glog.V(4).Infof("Scaled down the deployment %s and marked it as failed", deployutil.LabelForDeployment(deployment)) return nil }