diff --git a/pkg/cmd/cli/cmd/deploy.go b/pkg/cmd/cli/cmd/deploy.go index f0b2ae55062f..6e1769ad8d4a 100644 --- a/pkg/cmd/cli/cmd/deploy.go +++ b/pkg/cmd/cli/cmd/deploy.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "io" @@ -8,14 +9,28 @@ import ( kapi "github.com/GoogleCloudPlatform/kubernetes/pkg/api" kerrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + kclient "github.com/GoogleCloudPlatform/kubernetes/pkg/client" cmdutil "github.com/GoogleCloudPlatform/kubernetes/pkg/kubectl/cmd/util" + "github.com/openshift/origin/pkg/client" "github.com/openshift/origin/pkg/cmd/cli/describe" "github.com/openshift/origin/pkg/cmd/util/clientcmd" deployapi "github.com/openshift/origin/pkg/deploy/api" deployutil "github.com/openshift/origin/pkg/deploy/util" ) +type DeployOptions struct { + out io.Writer + osClient *client.Client + kubeClient *kclient.Client + namespace string + baseCommandName string + + deploymentConfigName string + deployLatest bool + retryDeploy bool +} + const ( deploy_long = `View, start and restart deployments. @@ -26,14 +41,18 @@ NOTE: This command is still under active development and is subject to change.` deploy_example = ` // Display the latest deployment for the 'database' deployment config $ %[1]s deploy database - // Start a new deployment based on the 'frontend' deployment config - $ %[1]s deploy frontend --latest` + // Start a new deployment based on the 'database' deployment config + $ %[1]s deploy frontend --latest + + // Retry the latest failed deployment based on the 'frontend' deployment config + $ %[1]s deploy frontend --retry` ) // NewCmdDeploy creates a new `deploy` command. func NewCmdDeploy(fullName string, f *clientcmd.Factory, out io.Writer) *cobra.Command { - var deployLatest bool - var retryDeploy bool + options := &DeployOptions{ + baseCommandName: fullName, + } cmd := &cobra.Command{ Use: "deploy DEPLOYMENTCONFIG", @@ -41,59 +60,95 @@ func NewCmdDeploy(fullName string, f *clientcmd.Factory, out io.Writer) *cobra.C Long: deploy_long, Example: fmt.Sprintf(deploy_example, fullName), Run: func(cmd *cobra.Command, args []string) { - if len(args) == 0 || len(args[0]) == 0 { - fmt.Println(cmdutil.UsageError(cmd, "A deploymentConfig name is required.")) - return + if err := options.Complete(f, args, out); err != nil { + cmdutil.CheckErr(err) } - if deployLatest && retryDeploy { - fmt.Println(cmdutil.UsageError(cmd, "Only one of --latest or --retry is allowed.")) - return + + if err := options.Validate(args); err != nil { + cmdutil.CheckErr(cmdutil.UsageError(cmd, err.Error())) } - configName := args[0] + if err := options.RunDeploy(); err != nil { + cmdutil.CheckErr(err) + } + }, + } - osClient, kubeClient, err := f.Clients() - cmdutil.CheckErr(err) + cmd.Flags().BoolVar(&options.deployLatest, "latest", false, "Start a new deployment now.") + cmd.Flags().BoolVar(&options.retryDeploy, "retry", false, "Retry the latest failed deployment.") - namespace, err := f.DefaultNamespace() - cmdutil.CheckErr(err) + return cmd +} - config, err := osClient.DeploymentConfigs(namespace).Get(configName) - cmdutil.CheckErr(err) +func (o *DeployOptions) Complete(f *clientcmd.Factory, args []string, out io.Writer) error { + var err error - commandClient := &deployCommandClientImpl{ - GetDeploymentFn: func(namespace, name string) (*kapi.ReplicationController, error) { - return kubeClient.ReplicationControllers(namespace).Get(name) - }, - UpdateDeploymentConfigFn: func(config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error) { - return osClient.DeploymentConfigs(config.Namespace).Update(config) - }, - UpdateDeploymentFn: func(deployment *kapi.ReplicationController) (*kapi.ReplicationController, error) { - return kubeClient.ReplicationControllers(deployment.Namespace).Update(deployment) - }, - } + o.osClient, o.kubeClient, err = f.Clients() + if err != nil { + return err + } + o.namespace, err = f.DefaultNamespace() + if err != nil { + return err + } - switch { - case deployLatest: - c := &deployLatestCommand{client: commandClient} - err = c.deploy(config, out) - case retryDeploy: - c := &retryDeploymentCommand{client: commandClient} - err = c.retry(config, out) - default: - describer := describe.NewLatestDeploymentDescriber(osClient, kubeClient) - desc, err := describer.Describe(config.Namespace, config.Name) - cmdutil.CheckErr(err) - fmt.Fprintln(out, desc) - } - cmdutil.CheckErr(err) + o.out = out + + if len(args) > 0 { + o.deploymentConfigName = args[0] + } + + return nil +} + +func (o DeployOptions) Validate(args []string) error { + if len(args) == 0 || len(args[0]) == 0 { + return errors.New("A deploymentConfig name is required.") + } + if len(args) > 1 { + return errors.New("Only one deploymentConfig name is supported as argument.") + } + if o.deployLatest && o.retryDeploy { + return errors.New("Only one of --latest or --retry is allowed.") + } + return nil +} + +func (o DeployOptions) RunDeploy() error { + config, err := o.osClient.DeploymentConfigs(o.namespace).Get(o.deploymentConfigName) + if err != nil { + return err + } + + commandClient := &deployCommandClientImpl{ + GetDeploymentFn: func(namespace, name string) (*kapi.ReplicationController, error) { + return o.kubeClient.ReplicationControllers(namespace).Get(name) + }, + UpdateDeploymentConfigFn: func(config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error) { + return o.osClient.DeploymentConfigs(config.Namespace).Update(config) + }, + UpdateDeploymentFn: func(deployment *kapi.ReplicationController) (*kapi.ReplicationController, error) { + return o.kubeClient.ReplicationControllers(deployment.Namespace).Update(deployment) }, } - cmd.Flags().BoolVar(&deployLatest, "latest", false, "Start a new deployment now.") - cmd.Flags().BoolVar(&retryDeploy, "retry", false, "Retry the latest failed deployment.") + switch { + case o.deployLatest: + c := &deployLatestCommand{client: commandClient} + err = c.deploy(config, o.out) + case o.retryDeploy: + c := &retryDeploymentCommand{client: commandClient} + err = c.retry(config, o.out) + default: + describer := describe.NewLatestDeploymentsDescriber(o.osClient, o.kubeClient, -1) + desc, err := describer.Describe(config.Namespace, config.Name) + if err != nil { + return err + } + fmt.Fprintln(o.out, desc) + } - return cmd + return err } // deployCommandClient abstracts access to the API server. diff --git a/pkg/cmd/cli/describe/deployments.go b/pkg/cmd/cli/describe/deployments.go index ad328c61dd56..e90d28fa21f8 100644 --- a/pkg/cmd/cli/describe/deployments.go +++ b/pkg/cmd/cli/describe/deployments.go @@ -3,6 +3,7 @@ package describe import ( "fmt" "io" + "sort" "strconv" "strings" "text/tabwriter" @@ -29,6 +30,7 @@ type DeploymentConfigDescriber struct { type deploymentDescriberClient interface { getDeploymentConfig(namespace, name string) (*deployapi.DeploymentConfig, error) getDeployment(namespace, name string) (*kapi.ReplicationController, error) + listDeployments(namespace string, selector labels.Selector) (*kapi.ReplicationControllerList, error) listPods(namespace string, selector labels.Selector) (*kapi.PodList, error) listEvents(deploymentConfig *deployapi.DeploymentConfig) (*kapi.EventList, error) } @@ -36,6 +38,7 @@ type deploymentDescriberClient interface { type genericDeploymentDescriberClient struct { getDeploymentConfigFunc func(namespace, name string) (*deployapi.DeploymentConfig, error) getDeploymentFunc func(namespace, name string) (*kapi.ReplicationController, error) + listDeploymentsFunc func(namespace string, selector labels.Selector) (*kapi.ReplicationControllerList, error) listPodsFunc func(namespace string, selector labels.Selector) (*kapi.PodList, error) listEventsFunc func(deploymentConfig *deployapi.DeploymentConfig) (*kapi.EventList, error) } @@ -48,6 +51,10 @@ func (c *genericDeploymentDescriberClient) getDeployment(namespace, name string) return c.getDeploymentFunc(namespace, name) } +func (c *genericDeploymentDescriberClient) listDeployments(namespace string, selector labels.Selector) (*kapi.ReplicationControllerList, error) { + return c.listDeploymentsFunc(namespace, selector) +} + func (c *genericDeploymentDescriberClient) listPods(namespace string, selector labels.Selector) (*kapi.PodList, error) { return c.listPodsFunc(namespace, selector) } @@ -84,6 +91,9 @@ func NewDeploymentConfigDescriber(client client.Interface, kclient kclient.Inter getDeploymentFunc: func(namespace, name string) (*kapi.ReplicationController, error) { return kclient.ReplicationControllers(namespace).Get(name) }, + listDeploymentsFunc: func(namespace string, selector labels.Selector) (*kapi.ReplicationControllerList, error) { + return kclient.ReplicationControllers(namespace).List(selector) + }, listPodsFunc: func(namespace string, selector labels.Selector) (*kapi.PodList, error) { return kclient.Pods(namespace).List(selector, fields.Everything()) }, @@ -129,7 +139,20 @@ func (d *DeploymentConfigDescriber) Describe(namespace, name string) (string, er formatString(out, "Latest Deployment", fmt.Sprintf("error: %v", err)) } } else { - printDeploymentRc(deployment, d.client, out) + header := fmt.Sprintf("Deployment #%v (latest)", deployment.Annotations[deployapi.DeploymentVersionAnnotation]) + printDeploymentRc(deployment, d.client, out, header, true) + } + deploymentsHistory, err := d.client.listDeployments(namespace, labels.Everything()) + if err == nil { + sorted := rcSorter{} + sorted = append(sorted, deploymentsHistory.Items...) + sort.Sort(sorted) + for _, item := range sorted { + if item.Name != deploymentName && deploymentConfig.Name == item.Annotations[deployapi.DeploymentConfigAnnotation] { + header := fmt.Sprintf("Deployment #%v", item.Annotations[deployapi.DeploymentVersionAnnotation]) + printDeploymentRc(&item, d.client, out, header, false) + } + } } if events != nil { @@ -216,19 +239,28 @@ func printReplicationControllerSpec(spec kapi.ReplicationControllerSpec, w io.Wr return nil } -func printDeploymentRc(deployment *kapi.ReplicationController, client deploymentDescriberClient, w io.Writer) error { - running, waiting, succeeded, failed, err := getPodStatusForDeployment(deployment, client) - if err != nil { - return err +func printDeploymentRc(deployment *kapi.ReplicationController, client deploymentDescriberClient, w io.Writer, header string, verbose bool) error { + if len(header) > 0 { + fmt.Fprintf(w, "%v:\n", header) } - fmt.Fprint(w, "Latest Deployment:\n") - fmt.Fprintf(w, "\tName:\t%s\n", deployment.Name) + if verbose { + fmt.Fprintf(w, "\tName:\t%s\n", deployment.Name) + } + timeAt := strings.ToLower(formatRelativeTime(deployment.CreationTimestamp.Time)) + fmt.Fprintf(w, "\tCreated:\t%s ago\n", timeAt) fmt.Fprintf(w, "\tStatus:\t%s\n", deployment.Annotations[deployapi.DeploymentStatusAnnotation]) - fmt.Fprintf(w, "\tSelector:\t%s\n", formatLabels(deployment.Spec.Selector)) - fmt.Fprintf(w, "\tLabels:\t%s\n", formatLabels(deployment.Labels)) fmt.Fprintf(w, "\tReplicas:\t%d current / %d desired\n", deployment.Status.Replicas, deployment.Spec.Replicas) - fmt.Fprintf(w, "\tPods Status:\t%d Running / %d Waiting / %d Succeeded / %d Failed\n", running, waiting, succeeded, failed) + + if verbose { + fmt.Fprintf(w, "\tSelector:\t%s\n", formatLabels(deployment.Spec.Selector)) + fmt.Fprintf(w, "\tLabels:\t%s\n", formatLabels(deployment.Labels)) + running, waiting, succeeded, failed, err := getPodStatusForDeployment(deployment, client) + if err != nil { + return err + } + fmt.Fprintf(w, "\tPods Status:\t%d Running / %d Waiting / %d Succeeded / %d Failed\n", running, waiting, succeeded, failed) + } return nil } @@ -253,12 +285,15 @@ func getPodStatusForDeployment(deployment *kapi.ReplicationController, client de return } -type LatestDeploymentDescriber struct { +type LatestDeploymentsDescriber struct { + count int client deploymentDescriberClient } -func NewLatestDeploymentDescriber(client client.Interface, kclient kclient.Interface) *LatestDeploymentDescriber { - return &LatestDeploymentDescriber{ +// List the latest deployments limited to "count". In case count == -1, list back to the last successful. +func NewLatestDeploymentsDescriber(client client.Interface, kclient kclient.Interface, count int) *LatestDeploymentsDescriber { + return &LatestDeploymentsDescriber{ + count: count, client: &genericDeploymentDescriberClient{ getDeploymentConfigFunc: func(namespace, name string) (*deployapi.DeploymentConfig, error) { return client.DeploymentConfigs(namespace).Get(name) @@ -266,6 +301,9 @@ func NewLatestDeploymentDescriber(client client.Interface, kclient kclient.Inter getDeploymentFunc: func(namespace, name string) (*kapi.ReplicationController, error) { return kclient.ReplicationControllers(namespace).Get(name) }, + listDeploymentsFunc: func(namespace string, selector labels.Selector) (*kapi.ReplicationControllerList, error) { + return kclient.ReplicationControllers(namespace).List(selector) + }, listPodsFunc: func(namespace string, selector labels.Selector) (*kapi.PodList, error) { return kclient.Pods(namespace).List(selector, fields.Everything()) }, @@ -276,62 +314,55 @@ func NewLatestDeploymentDescriber(client client.Interface, kclient kclient.Inter } } -func (d *LatestDeploymentDescriber) Describe(namespace, name string) (string, error) { +func (d *LatestDeploymentsDescriber) Describe(namespace, name string) (string, error) { config, err := d.client.getDeploymentConfig(namespace, name) if err != nil { return "", err } - deploymentName := deployutil.LatestDeploymentNameForConfig(config) - deployment, err := d.client.getDeployment(config.Namespace, deploymentName) - if err != nil && !kerrors.IsNotFound(err) { - return "", err + var deployments []kapi.ReplicationController + if d.count == -1 || d.count > 1 { + list, err := d.client.listDeployments(namespace, labels.Everything()) + if err != nil && !kerrors.IsNotFound(err) { + return "", err + } + deployments = list.Items + } else { + deploymentName := deployutil.LatestDeploymentNameForConfig(config) + deployment, err := d.client.getDeployment(config.Namespace, deploymentName) + if err != nil && !kerrors.IsNotFound(err) { + return "", err + } + if deployment != nil { + deployments = []kapi.ReplicationController{*deployment} + } } g := graph.New() deploy := graph.DeploymentConfig(g, config) - if deployment != nil { - graph.JoinDeployments(deploy.(*graph.DeploymentConfigNode), []kapi.ReplicationController{*deployment}) + if len(deployments) > 0 { + graph.JoinDeployments(deploy.(*graph.DeploymentConfigNode), deployments) } return tabbedString(func(out *tabwriter.Writer) error { - indent := " " - fmt.Fprintf(out, "Latest deployment for %s/%s:\n", namespace, name) - printLines(out, indent, 1, d.describeDeployment(deploy.(*graph.DeploymentConfigNode))...) + node := deploy.(*graph.DeploymentConfigNode) + descriptions := describeDeployments(node, d.count) + for i, description := range descriptions { + descriptions[i] = fmt.Sprintf("%v %v", name, description) + } + printLines(out, "", 0, descriptions...) return nil }) } -func (d *LatestDeploymentDescriber) describeDeployment(node *graph.DeploymentConfigNode) []string { - if node == nil { - return nil - } - out := []string{} - - if node.ActiveDeployment == nil { - on, auto := describeDeploymentConfigTriggers(node.DeploymentConfig) - if node.DeploymentConfig.LatestVersion == 0 { - out = append(out, fmt.Sprintf("#1 waiting %s. Run osc deploy --latest to deploy now.", on)) - } else if auto { - out = append(out, fmt.Sprintf("#%d pending %s. Run osc deploy --latest to deploy now.", node.DeploymentConfig.LatestVersion, on)) - } - // TODO: detect new image available? - } else { - out = append(out, d.describeDeploymentStatus(node.ActiveDeployment)) - } - return out -} +type rcSorter []kapi.ReplicationController -func (d *LatestDeploymentDescriber) describeDeploymentStatus(deploy *kapi.ReplicationController) string { - timeAt := strings.ToLower(formatRelativeTime(deploy.CreationTimestamp.Time)) - switch s := deploy.Annotations[deployapi.DeploymentStatusAnnotation]; deployapi.DeploymentStatus(s) { - case deployapi.DeploymentStatusFailed: - // TODO: encode fail time in the rc - return fmt.Sprintf("#%s failed %s ago. You can restart this deployment with osc deploy --retry.", deploy.Annotations[deployapi.DeploymentVersionAnnotation], timeAt) - case deployapi.DeploymentStatusComplete: - // TODO: pod status output - return fmt.Sprintf("#%s deployed %s ago", deploy.Annotations[deployapi.DeploymentVersionAnnotation], timeAt) - default: - return fmt.Sprintf("#%s deployment %s %s ago", deploy.Annotations[deployapi.DeploymentVersionAnnotation], strings.ToLower(s), timeAt) - } +func (s rcSorter) Len() int { + return len(s) +} +func (s rcSorter) Less(i, j int) bool { + return s[i].CreationTimestamp.Unix() > s[j].CreationTimestamp.Unix() +} +func (s rcSorter) Swap(i, j int) { + s[i], s[j] = s[j], s[i] } diff --git a/pkg/cmd/cli/describe/describer_test.go b/pkg/cmd/cli/describe/describer_test.go index df665c2eb25c..a4daeb61ea1a 100644 --- a/pkg/cmd/cli/describe/describer_test.go +++ b/pkg/cmd/cli/describe/describer_test.go @@ -76,6 +76,7 @@ func TestDeploymentConfigDescriber(t *testing.T) { deployment, _ := deployutil.MakeDeployment(config, kapi.Codec) podList := &kapi.PodList{} eventList := &kapi.EventList{} + deploymentList := &kapi.ReplicationControllerList{} d := &DeploymentConfigDescriber{ client: &genericDeploymentDescriberClient{ @@ -85,6 +86,9 @@ func TestDeploymentConfigDescriber(t *testing.T) { getDeploymentFunc: func(namespace, name string) (*kapi.ReplicationController, error) { return deployment, nil }, + listDeploymentsFunc: func(namespace string, selector labels.Selector) (*kapi.ReplicationControllerList, error) { + return deploymentList, nil + }, listPodsFunc: func(namespace string, selector labels.Selector) (*kapi.PodList, error) { return podList, nil }, diff --git a/pkg/cmd/cli/describe/projectstatus.go b/pkg/cmd/cli/describe/projectstatus.go index 210ccf3a3c83..87004c974920 100644 --- a/pkg/cmd/cli/describe/projectstatus.go +++ b/pkg/cmd/cli/describe/projectstatus.go @@ -355,24 +355,34 @@ func describeDeployments(node *graph.DeploymentConfigNode, count int) []string { return nil } out := []string{} + deployments := node.Deployments if node.ActiveDeployment == nil { on, auto := describeDeploymentConfigTriggers(node.DeploymentConfig) if node.DeploymentConfig.LatestVersion == 0 { - out = append(out, fmt.Sprintf("#1 deployment waiting %s", on)) + out = append(out, fmt.Sprintf("#1 deployment waiting %s. Run osc deploy --latest to deploy now.", on)) } else if auto { - out = append(out, fmt.Sprintf("#%d deployment pending %s", node.DeploymentConfig.LatestVersion, on)) + out = append(out, fmt.Sprintf("#%d deployment pending %s. Run osc deploy --latest to deploy now.", node.DeploymentConfig.LatestVersion, on)) } // TODO: detect new image available? } else { - out = append(out, describeDeploymentStatus(node.ActiveDeployment)) - count-- + deployments = append([]*kapi.ReplicationController{node.ActiveDeployment}, deployments...) } - for i, deployment := range node.Deployments { - if i >= count { - break - } + + for i, deployment := range deployments { out = append(out, describeDeploymentStatus(deployment)) + + switch { + case count == -1: + status := deployment.Annotations[deployapi.DeploymentStatusAnnotation] + if deployapi.DeploymentStatus(status) == deployapi.DeploymentStatusComplete { + return out + } + default: + if i+1 >= count { + return out + } + } } return out } @@ -382,7 +392,7 @@ func describeDeploymentStatus(deploy *kapi.ReplicationController) string { switch s := deploy.Annotations[deployapi.DeploymentStatusAnnotation]; deployapi.DeploymentStatus(s) { case deployapi.DeploymentStatusFailed: // TODO: encode fail time in the rc - return fmt.Sprintf("#%s deployment failed %s ago", deploy.Annotations[deployapi.DeploymentVersionAnnotation], timeAt) + return fmt.Sprintf("#%s deployment failed %s ago. You can restart this deployment with osc deploy --retry.", deploy.Annotations[deployapi.DeploymentVersionAnnotation], timeAt) case deployapi.DeploymentStatusComplete: // TODO: pod status output return fmt.Sprintf("#%s deployed %s ago", deploy.Annotations[deployapi.DeploymentVersionAnnotation], timeAt)