pkg/cmd/cli/cmd/deploy.go
349e59a9
 package cmd
 
 import (
b217ed60
 	"errors"
349e59a9
 	"fmt"
 	"io"
6ef537c4
 	"sort"
e32d7417
 	"strconv"
 	"strings"
6ef537c4
 
21fb8569
 	"time"
349e59a9
 
f451d174
 	units "github.com/docker/go-units"
349e59a9
 	"github.com/spf13/cobra"
 
83c702b4
 	kapi "k8s.io/kubernetes/pkg/api"
 	kerrors "k8s.io/kubernetes/pkg/api/errors"
3dd75654
 	kclient "k8s.io/kubernetes/pkg/client/unversioned"
95ec120f
 	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
9bea8db2
 	"k8s.io/kubernetes/pkg/kubectl/resource"
349e59a9
 
b217ed60
 	"github.com/openshift/origin/pkg/client"
349e59a9
 	"github.com/openshift/origin/pkg/cmd/cli/describe"
6267dded
 	"github.com/openshift/origin/pkg/cmd/templates"
349e59a9
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
 	deployapi "github.com/openshift/origin/pkg/deploy/api"
 	deployutil "github.com/openshift/origin/pkg/deploy/util"
 )
 
5fab300d
 // DeployOptions holds all the options for the `deploy` command
b217ed60
 type DeployOptions struct {
 	out             io.Writer
9bea8db2
 	osClient        client.Interface
 	kubeClient      kclient.Interface
 	builder         *resource.Builder
b217ed60
 	namespace       string
 	baseCommandName string
 
 	deploymentConfigName string
 	deployLatest         bool
 	retryDeploy          bool
e32d7417
 	cancelDeploy         bool
9bcc8d14
 	enableTriggers       bool
11e813f5
 	follow               bool
b217ed60
 }
 
6267dded
 var (
 	deployLong = templates.LongDesc(`
 		View, start, cancel, or retry a deployment
349e59a9
 
6267dded
 		This command allows you to control a deployment config. Each individual deployment is exposed
 		as a new replication controller, and the deployment process manages scaling down old deployments
 		and scaling up new ones. Use '%[1]s rollback' to rollback to any previous deployment.
349e59a9
 
6267dded
 		There are several deployment strategies defined:
1558f2d9
 
6267dded
 		* Rolling (default) - scales up the new deployment in stages, gradually reducing the number
 		  of old deployments. If one of the new deployed pods never becomes "ready", the new deployment
 		  will be rolled back (scaled down to zero). Use when your application can tolerate two versions
 		  of code running at the same time (many web applications, scalable databases)
 		* Recreate - scales the old deployment down to zero, then scales the new deployment up to full.
 		  Use when your application cannot tolerate two versions of code running at the same time
 		* Custom - run your own deployment process inside a Docker container using your own scripts.
c124965f
 
6267dded
 		If a deployment fails, you may opt to retry it (if the error was transient). Some deployments may
 		never successfully complete - in which case you can use the '--latest' flag to force a redeployment.
 		If a deployment config has completed deploying successfully at least once in the past, it would be
 		automatically rolled back in the event of a new failed deployment. Note that you would still need
 		to update the erroneous deployment config in order to have its template persisted across your
 		application.
c124965f
 
6267dded
 		If you want to cancel a running deployment, use '--cancel' but keep in mind that this is a best-effort
 		operation and may take some time to complete. It’s possible the deployment will partially or totally
 		complete before the cancellation is effective. In such a case an appropriate event will be emitted.
0ec0c6c8
 
6267dded
 		If no options are given, shows information about the latest deployment.`)
c124965f
 
6267dded
 	deployExample = templates.Examples(`
 		# Display the latest deployment for the 'database' deployment config
 	  %[1]s deploy database
1558f2d9
 
6267dded
 	  # Start a new deployment based on the 'database'
 	  %[1]s deploy database --latest
b217ed60
 
6267dded
 	  # Start a new deployment and follow its log
 	  %[1]s deploy database --latest --follow
11e813f5
 
6267dded
 	  # Retry the latest failed deployment based on 'frontend'
 	  # The deployer pod and any hook pods are deleted for the latest failed deployment
 	  %[1]s deploy frontend --retry
e32d7417
 
6267dded
 	  # Cancel the in-progress deployment based on 'frontend'
 	  %[1]s deploy frontend --cancel`)
1558f2d9
 )
349e59a9
 
 // NewCmdDeploy creates a new `deploy` command.
 func NewCmdDeploy(fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command {
b217ed60
 	options := &DeployOptions{
 		baseCommandName: fullName,
 	}
349e59a9
 
 	cmd := &cobra.Command{
375a4103
 		Use:        "deploy DEPLOYMENTCONFIG [--latest|--retry|--cancel|--enable-triggers]",
b3e77d8d
 		Short:      "View, start, cancel, or retry a deployment",
d2a1659f
 		Long:       fmt.Sprintf(deployLong, fullName),
b3e77d8d
 		Example:    fmt.Sprintf(deployExample, fullName),
 		SuggestFor: []string{"deployment"},
349e59a9
 		Run: func(cmd *cobra.Command, args []string) {
b217ed60
 			if err := options.Complete(f, args, out); err != nil {
95ec120f
 				kcmdutil.CheckErr(err)
349e59a9
 			}
b217ed60
 
375a4103
 			if err := options.Validate(); err != nil {
95ec120f
 				kcmdutil.CheckErr(kcmdutil.UsageError(cmd, err.Error()))
349e59a9
 			}
 
b217ed60
 			if err := options.RunDeploy(); err != nil {
95ec120f
 				kcmdutil.CheckErr(err)
b217ed60
 			}
 		},
 	}
349e59a9
 
b217ed60
 	cmd.Flags().BoolVar(&options.deployLatest, "latest", false, "Start a new deployment now.")
cc7cbc20
 	cmd.Flags().MarkDeprecated("latest", fmt.Sprintf("use '%s rollout latest' instead", fullName))
b217ed60
 	cmd.Flags().BoolVar(&options.retryDeploy, "retry", false, "Retry the latest failed deployment.")
e32d7417
 	cmd.Flags().BoolVar(&options.cancelDeploy, "cancel", false, "Cancel the in-progress deployment.")
9bcc8d14
 	cmd.Flags().BoolVar(&options.enableTriggers, "enable-triggers", false, "Enables all image triggers for the deployment config.")
11e813f5
 	cmd.Flags().BoolVar(&options.follow, "follow", false, "Follow the logs of a deployment")
349e59a9
 
b217ed60
 	return cmd
 }
349e59a9
 
b217ed60
 func (o *DeployOptions) Complete(f *clientcmd.Factory, args []string, out io.Writer) error {
375a4103
 	if len(args) > 1 {
e30dccdf
 		return errors.New("only one deployment config name is supported as argument.")
375a4103
 	}
b217ed60
 	var err error
349e59a9
 
b217ed60
 	o.osClient, o.kubeClient, err = f.Clients()
 	if err != nil {
 		return err
 	}
ab7d732c
 	o.namespace, _, err = f.DefaultNamespace()
b217ed60
 	if err != nil {
 		return err
 	}
349e59a9
 
59e6f9d2
 	mapper, typer := f.Object(false)
a3581b14
 	o.builder = resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), kapi.Codecs.UniversalDecoder())
9bea8db2
 
b217ed60
 	o.out = out
 
 	if len(args) > 0 {
e30dccdf
 		o.deploymentConfigName = args[0]
b217ed60
 	}
 
 	return nil
 }
 
375a4103
 func (o DeployOptions) Validate() error {
 	if len(o.deploymentConfigName) == 0 {
a4812504
 		msg := fmt.Sprintf("a deployment config name is required.\nUse \"%s get dc\" for a list of available deployment configs.", o.baseCommandName)
 		return errors.New(msg)
b217ed60
 	}
e32d7417
 	numOptions := 0
 	if o.deployLatest {
 		numOptions++
 	}
 	if o.retryDeploy {
 		numOptions++
 	}
 	if o.cancelDeploy {
11e813f5
 		if o.follow {
 			return errors.New("cannot follow the logs while canceling a deployment")
 		}
e32d7417
 		numOptions++
 	}
b74030e0
 	if o.enableTriggers {
11e813f5
 		if o.follow {
 			return errors.New("cannot follow the logs while enabling triggers for a deployment")
 		}
b74030e0
 		numOptions++
 	}
e32d7417
 	if numOptions > 1 {
b74030e0
 		return errors.New("only one of --latest, --retry, --cancel, or --enable-triggers is allowed.")
b217ed60
 	}
 	return nil
 }
 
375a4103
 func (o DeployOptions) RunDeploy() error {
9bea8db2
 	r := o.builder.
 		NamespaceParam(o.namespace).
e30dccdf
 		ResourceNames("deploymentconfigs", o.deploymentConfigName).
9bea8db2
 		SingleResourceType().
 		Do()
 	resultObj, err := r.Object()
b217ed60
 	if err != nil {
 		return err
 	}
9bea8db2
 	config, ok := resultObj.(*deployapi.DeploymentConfig)
 	if !ok {
e30dccdf
 		return fmt.Errorf("%s is not a valid deployment config", o.deploymentConfigName)
349e59a9
 	}
 
b217ed60
 	switch {
 	case o.deployLatest:
11e813f5
 		err = o.deploy(config)
b217ed60
 	case o.retryDeploy:
11e813f5
 		err = o.retry(config)
e32d7417
 	case o.cancelDeploy:
11e813f5
 		err = o.cancel(config)
9bcc8d14
 	case o.enableTriggers:
11e813f5
 		err = o.reenableTriggers(config)
b217ed60
 	default:
11e813f5
 		if o.follow {
 			return o.getLogs(config)
 		}
b217ed60
 		describer := describe.NewLatestDeploymentsDescriber(o.osClient, o.kubeClient, -1)
 		desc, err := describer.Describe(config.Namespace, config.Name)
 		if err != nil {
 			return err
 		}
db00da80
 		fmt.Fprint(o.out, desc)
b217ed60
 	}
349e59a9
 
b217ed60
 	return err
349e59a9
 }
 
 // deploy launches a new deployment unless there's already a deployment
 // process in progress for config.
11e813f5
 func (o DeployOptions) deploy(config *deployapi.DeploymentConfig) error {
9b2c4ab9
 	if config.Spec.Paused {
 		return fmt.Errorf("cannot deploy a paused deployment config")
 	}
dda90ccb
 	// TODO: This implies that deploymentconfig.status.latestVersion is always synced. Currently,
 	// that's the case because clients (oc, trigger controllers) are updating the status directly.
 	// Clients should be acting either on spec or on annotations and status updates should be a
 	// responsibility of the main controller. We need to start by unplugging this assumption from
 	// our client tools.
349e59a9
 	deploymentName := deployutil.LatestDeploymentNameForConfig(config)
9bea8db2
 	deployment, err := o.kubeClient.ReplicationControllers(config.Namespace).Get(deploymentName)
db550ba4
 	if err == nil && !deployutil.IsTerminatedDeployment(deployment) {
9298d0b8
 		// Reject attempts to start a concurrent deployment.
db550ba4
 		return fmt.Errorf("#%d is already in progress (%s).\nOptionally, you can cancel this deployment using the --cancel option.",
 			config.Status.LatestVersion, deployutil.DeploymentStatusFor(deployment))
 	}
 	if err != nil && !kerrors.IsNotFound(err) {
 		return err
349e59a9
 	}
 
9acfba52
 	request := &deployapi.DeploymentRequest{
 		Name:   config.Name,
 		Latest: false,
 		Force:  true,
 	}
 
 	dc, err := o.osClient.DeploymentConfigs(config.Namespace).Instantiate(request)
 	// Pre 1.4 servers don't support the instantiate endpoint. Fallback to incrementing
 	// latestVersion on them.
 	if kerrors.IsNotFound(err) || kerrors.IsForbidden(err) {
 		config.Status.LatestVersion++
 		dc, err = o.osClient.DeploymentConfigs(config.Namespace).Update(config)
 	}
dda90ccb
 	if err != nil {
02a2d626
 		if kerrors.IsBadRequest(err) {
 			err = fmt.Errorf("%v - try 'oc rollout latest dc/%s'", err, config.Name)
 		}
dda90ccb
 		return err
 	}
11e813f5
 	fmt.Fprintf(o.out, "Started deployment #%d\n", dc.Status.LatestVersion)
 	if o.follow {
 		return o.getLogs(dc)
 	}
 	fmt.Fprintf(o.out, "Use '%s logs -f dc/%s' to track its progress.\n", o.baseCommandName, dc.Name)
dda90ccb
 	return nil
349e59a9
 }
 
 // retry resets the status of the latest deployment to New, which will cause
 // the deployment to be retried. An error is returned if the deployment is not
 // currently in a failed state.
11e813f5
 func (o DeployOptions) retry(config *deployapi.DeploymentConfig) error {
9b2c4ab9
 	if config.Spec.Paused {
 		return fmt.Errorf("cannot retry a paused deployment config")
 	}
05e20525
 	if config.Status.LatestVersion == 0 {
794886ec
 		return fmt.Errorf("no deployments found for %s/%s", config.Namespace, config.Name)
349e59a9
 	}
dda90ccb
 	// TODO: This implies that deploymentconfig.status.latestVersion is always synced. Currently,
 	// that's the case because clients (oc, trigger controllers) are updating the status directly.
 	// Clients should be acting either on spec or on annotations and status updates should be a
 	// responsibility of the main controller. We need to start by unplugging this assumption from
 	// our client tools.
349e59a9
 	deploymentName := deployutil.LatestDeploymentNameForConfig(config)
9bea8db2
 	deployment, err := o.kubeClient.ReplicationControllers(config.Namespace).Get(deploymentName)
349e59a9
 	if err != nil {
794886ec
 		if kerrors.IsNotFound(err) {
db550ba4
 			return fmt.Errorf("unable to find the latest deployment (#%d).\nYou can start a new deployment with 'oc deploy --latest dc/%s'.", config.Status.LatestVersion, config.Name)
794886ec
 		}
349e59a9
 		return err
 	}
 
db550ba4
 	if !deployutil.IsFailedDeployment(deployment) {
 		message := fmt.Sprintf("#%d is %s; only failed deployments can be retried.\n", config.Status.LatestVersion, deployutil.DeploymentStatusFor(deployment))
 		if deployutil.IsCompleteDeployment(deployment) {
 			message += fmt.Sprintf("You can start a new deployment with 'oc deploy --latest dc/%s'.", config.Name)
794886ec
 		} else {
db550ba4
 			message += fmt.Sprintf("Optionally, you can cancel this deployment with 'oc deploy --cancel dc/%s'.", config.Name)
794886ec
 		}
 
 		return fmt.Errorf(message)
349e59a9
 	}
 
9e33ec11
 	// Delete the deployer pod as well as the deployment hooks pods, if any
f638b86d
 	pods, err := o.kubeClient.Pods(config.Namespace).List(kapi.ListOptions{LabelSelector: deployutil.DeployerPodSelector(deploymentName)})
9e33ec11
 	if err != nil {
05e20525
 		return fmt.Errorf("failed to list deployer/hook pods for deployment #%d: %v", config.Status.LatestVersion, err)
9e33ec11
 	}
 	for _, pod := range pods.Items {
9bea8db2
 		err := o.kubeClient.Pods(pod.Namespace).Delete(pod.Name, kapi.NewDeleteOptions(0))
9e33ec11
 		if err != nil {
05e20525
 			return fmt.Errorf("failed to delete deployer/hook pod %s for deployment #%d: %v", pod.Name, config.Status.LatestVersion, err)
9e33ec11
 		}
 	}
 
349e59a9
 	deployment.Annotations[deployapi.DeploymentStatusAnnotation] = string(deployapi.DeploymentStatusNew)
9e33ec11
 	// clear out the cancellation flag as well as any previous status-reason annotation
 	delete(deployment.Annotations, deployapi.DeploymentStatusReasonAnnotation)
 	delete(deployment.Annotations, deployapi.DeploymentCancelledAnnotation)
9bea8db2
 	_, err = o.kubeClient.ReplicationControllers(deployment.Namespace).Update(deployment)
11e813f5
 	if err != nil {
 		return err
349e59a9
 	}
11e813f5
 	fmt.Fprintf(o.out, "Retried #%d\n", config.Status.LatestVersion)
 	if o.follow {
 		return o.getLogs(config)
 	}
 	fmt.Fprintf(o.out, "Use '%s logs -f dc/%s' to track its progress.\n", o.baseCommandName, config.Name)
 	return nil
349e59a9
 }
 
e32d7417
 // cancel cancels any deployment process in progress for config.
11e813f5
 func (o DeployOptions) cancel(config *deployapi.DeploymentConfig) error {
9b2c4ab9
 	if config.Spec.Paused {
 		return fmt.Errorf("cannot cancel a paused deployment config")
 	}
f638b86d
 	deployments, err := o.kubeClient.ReplicationControllers(config.Namespace).List(kapi.ListOptions{LabelSelector: deployutil.ConfigSelector(config.Name)})
e32d7417
 	if err != nil {
 		return err
 	}
d4938135
 	if len(deployments.Items) == 0 {
11e813f5
 		fmt.Fprintf(o.out, "There have been no deployments for %s/%s\n", config.Namespace, config.Name)
d4938135
 		return nil
 	}
0b38d810
 	sort.Sort(deployutil.ByLatestVersionDesc(deployments.Items))
e32d7417
 	failedCancellations := []string{}
7693f015
 	anyCancelled := false
e32d7417
 	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
9bea8db2
 			_, err := o.kubeClient.ReplicationControllers(deployment.Namespace).Update(&deployment)
e32d7417
 			if err == nil {
11e813f5
 				fmt.Fprintf(o.out, "Cancelled deployment #%d\n", config.Status.LatestVersion)
7693f015
 				anyCancelled = true
e32d7417
 			} else {
11e813f5
 				fmt.Fprintf(o.out, "Couldn't cancel deployment #%d (status: %s): %v\n", deployutil.DeploymentVersionFor(&deployment), status, err)
2429a35c
 				failedCancellations = append(failedCancellations, strconv.FormatInt(deployutil.DeploymentVersionFor(&deployment), 10))
e32d7417
 			}
 		}
 	}
7693f015
 	if len(failedCancellations) > 0 {
e32d7417
 		return fmt.Errorf("couldn't cancel deployment %s", strings.Join(failedCancellations, ", "))
 	}
7693f015
 	if !anyCancelled {
6ef537c4
 		latest := &deployments.Items[0]
95f1ffa3
 		maybeCancelling := ""
 		if deployutil.IsDeploymentCancelled(latest) && !deployutil.IsTerminatedDeployment(latest) {
 			maybeCancelling = " (cancelling)"
0ec0c6c8
 		}
6ef537c4
 		timeAt := strings.ToLower(units.HumanDuration(time.Now().Sub(latest.CreationTimestamp.Time)))
11e813f5
 		fmt.Fprintf(o.out, "No deployments are in progress (latest deployment #%d %s%s %s ago)\n",
6ef537c4
 			deployutil.DeploymentVersionFor(latest),
95f1ffa3
 			strings.ToLower(string(deployutil.DeploymentStatusFor(latest))),
 			maybeCancelling,
6ef537c4
 			timeAt)
7693f015
 	}
 	return nil
e32d7417
 }
 
9bea8db2
 // reenableTriggers enables all image triggers and then persists config.
11e813f5
 func (o DeployOptions) reenableTriggers(config *deployapi.DeploymentConfig) error {
9bcc8d14
 	enabled := []string{}
05e20525
 	for _, trigger := range config.Spec.Triggers {
9bcc8d14
 		if trigger.Type == deployapi.DeploymentTriggerOnImageChange {
 			trigger.ImageChangeParams.Automatic = true
 			enabled = append(enabled, trigger.ImageChangeParams.From.Name)
 		}
 	}
 	if len(enabled) == 0 {
11e813f5
 		fmt.Fprintln(o.out, "No image triggers found to enable")
9bcc8d14
 		return nil
 	}
9bea8db2
 	_, err := o.osClient.DeploymentConfigs(config.Namespace).Update(config)
9bcc8d14
 	if err != nil {
 		return err
 	}
11e813f5
 	fmt.Fprintf(o.out, "Enabled image triggers: %s\n", strings.Join(enabled, ","))
9bcc8d14
 	return nil
 }
11e813f5
 
 func (o DeployOptions) getLogs(config *deployapi.DeploymentConfig) error {
 	opts := deployapi.DeploymentLogOptions{
 		Follow: true,
 	}
 	readCloser, err := o.osClient.DeploymentLogs(config.Namespace).Get(config.Name, opts).Stream()
 	if err != nil {
 		return err
 	}
 	defer readCloser.Close()
 	_, err = io.Copy(o.out, readCloser)
 	return err
 }