From f788c527abfadf3334ddb6f886964c63ce8c407a Mon Sep 17 00:00:00 2001 From: Michail Kargakis Date: Wed, 22 Jun 2016 13:27:38 +0200 Subject: [PATCH 1/2] deploy: update the log registry to poll on notfound deployments --- pkg/deploy/registry/deploylog/rest.go | 43 +++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/pkg/deploy/registry/deploylog/rest.go b/pkg/deploy/registry/deploylog/rest.go index 6739d0b753f3..63519ab20334 100644 --- a/pkg/deploy/registry/deploylog/rest.go +++ b/pkg/deploy/registry/deploylog/rest.go @@ -17,6 +17,7 @@ import ( genericrest "k8s.io/kubernetes/pkg/registry/generic/rest" "k8s.io/kubernetes/pkg/registry/pod" "k8s.io/kubernetes/pkg/runtime" + "k8s.io/kubernetes/pkg/util/wait" "github.com/openshift/origin/pkg/client" deployapi "github.com/openshift/origin/pkg/deploy/api" @@ -25,8 +26,12 @@ import ( deployutil "github.com/openshift/origin/pkg/deploy/util" ) -// defaultTimeout is the default time to wait for the logs of a deployment -const defaultTimeout time.Duration = 20 * time.Second +const ( + // defaultTimeout is the default time to wait for the logs of a deployment. + defaultTimeout time.Duration = 20 * time.Second + // defaultInterval is the default interval for polling a not found deployment. + defaultInterval time.Duration = 1 * time.Second +) // podGetter implements the ResourceGetter interface. Used by LogLocation to // retrieve the deployer pod @@ -50,6 +55,7 @@ type REST struct { pn unversioned.PodsNamespacer connInfo kubeletclient.ConnectionInfoGetter timeout time.Duration + interval time.Duration } // REST implements GetterWithOptions @@ -66,6 +72,7 @@ func NewREST(dn client.DeploymentConfigsNamespacer, rn unversioned.ReplicationCo pn: pn, connInfo: connectionInfo, timeout: defaultTimeout, + interval: defaultInterval, } } @@ -126,10 +133,9 @@ func (r *REST) Get(ctx kapi.Context, name string, opts runtime.Object) (runtime. // Get desired deployment targetName := deployutil.DeploymentNameForConfigVersion(config.Name, desiredVersion) - target, err := r.rn.ReplicationControllers(namespace).Get(targetName) + target, err := r.waitForExistingDeployment(namespace, targetName) if err != nil { - // TODO: Better error handling - return nil, errors.NewNotFound(kapi.Resource("replicationcontroller"), name) + return nil, err } podName := deployutil.DeployerPodNameForDeployment(target.Name) @@ -150,7 +156,7 @@ func (r *REST) Get(ctx kapi.Context, name string, opts runtime.Object) (runtime. return nil, errors.NewBadRequest(fmt.Sprintf("unable to wait for deployment %s to run: %v", deployutil.LabelForDeployment(target), err)) } if !ok { - return nil, errors.NewTimeoutError(fmt.Sprintf("timed out waiting for deployment %s to start after %s", deployutil.LabelForDeployment(target), r.timeout), 1) + return nil, errors.NewServerTimeout(kapi.Resource("ReplicationController"), "get", 2) } if deployutil.DeploymentStatusFor(latest) == deployapi.DeploymentStatusComplete { podName, err = r.returnApplicationPodName(target) @@ -180,6 +186,31 @@ func (r *REST) Get(ctx kapi.Context, name string, opts runtime.Object) (runtime. }, nil } +// waitForExistingDeployment will use the timeout to wait for a deployment to appear. +func (r *REST) waitForExistingDeployment(namespace, name string) (*kapi.ReplicationController, error) { + var ( + target *kapi.ReplicationController + err error + ) + + condition := func() (bool, error) { + target, err = r.rn.ReplicationControllers(namespace).Get(name) + switch { + case errors.IsNotFound(err): + return false, nil + case err != nil: + return false, err + } + return true, nil + } + + err = wait.PollImmediate(r.interval, r.timeout, condition) + if err == wait.ErrWaitTimeout { + err = errors.NewNotFound(kapi.Resource("replicationcontrollers"), name) + } + return target, err +} + // returnApplicationPodName returns the best candidate pod for the target deployment in order to // view its logs. func (r *REST) returnApplicationPodName(target *kapi.ReplicationController) (string, error) { From 11e813f56475cc509205232d8c5466740eb8d9c9 Mon Sep 17 00:00:00 2001 From: Michail Kargakis Date: Wed, 22 Jun 2016 13:00:10 +0200 Subject: [PATCH 2/2] oc: enable following deployment logs in deploy --- contrib/completions/bash/oc | 1 + contrib/completions/bash/openshift | 1 + contrib/completions/zsh/oc | 1 + contrib/completions/zsh/openshift | 1 + docs/generated/oc_by_example_content.adoc | 3 + docs/man/man1/oc-deploy.1 | 7 +++ docs/man/man1/openshift-cli-deploy.1 | 7 +++ pkg/cmd/cli/cmd/deploy.go | 73 +++++++++++++++++------ pkg/cmd/cli/cmd/deploy_test.go | 28 ++++----- test/extended/deployments/deployments.go | 9 +-- 10 files changed, 91 insertions(+), 40 deletions(-) diff --git a/contrib/completions/bash/oc b/contrib/completions/bash/oc index acc3e2b93ac1..64ea0439336c 100644 --- a/contrib/completions/bash/oc +++ b/contrib/completions/bash/oc @@ -1137,6 +1137,7 @@ _oc_deploy() flags+=("--cancel") flags+=("--enable-triggers") + flags+=("--follow") flags+=("--latest") flags+=("--retry") flags+=("--api-version=") diff --git a/contrib/completions/bash/openshift b/contrib/completions/bash/openshift index b75f1ae9048b..e74a6f1f6453 100644 --- a/contrib/completions/bash/openshift +++ b/contrib/completions/bash/openshift @@ -5306,6 +5306,7 @@ _openshift_cli_deploy() flags+=("--cancel") flags+=("--enable-triggers") + flags+=("--follow") flags+=("--latest") flags+=("--retry") flags+=("--api-version=") diff --git a/contrib/completions/zsh/oc b/contrib/completions/zsh/oc index 958169118086..917134cba1c8 100644 --- a/contrib/completions/zsh/oc +++ b/contrib/completions/zsh/oc @@ -1298,6 +1298,7 @@ _oc_deploy() flags+=("--cancel") flags+=("--enable-triggers") + flags+=("--follow") flags+=("--latest") flags+=("--retry") flags+=("--api-version=") diff --git a/contrib/completions/zsh/openshift b/contrib/completions/zsh/openshift index f7682f88e72d..14e490d6163d 100644 --- a/contrib/completions/zsh/openshift +++ b/contrib/completions/zsh/openshift @@ -5467,6 +5467,7 @@ _openshift_cli_deploy() flags+=("--cancel") flags+=("--enable-triggers") + flags+=("--follow") flags+=("--latest") flags+=("--retry") flags+=("--api-version=") diff --git a/docs/generated/oc_by_example_content.adoc b/docs/generated/oc_by_example_content.adoc index 1486ac843234..955ceabffef4 100644 --- a/docs/generated/oc_by_example_content.adoc +++ b/docs/generated/oc_by_example_content.adoc @@ -1250,6 +1250,9 @@ View, start, cancel, or retry a deployment # Start a new deployment based on the 'database' oc deploy database --latest + # Start a new deployment and follow its log + oc deploy database --latest --follow + # Retry the latest failed deployment based on 'frontend' # The deployer pod and any hook pods are deleted for the latest failed deployment oc deploy frontend --retry diff --git a/docs/man/man1/oc-deploy.1 b/docs/man/man1/oc-deploy.1 index 83b057c73aff..b34e615ac243 100644 --- a/docs/man/man1/oc-deploy.1 +++ b/docs/man/man1/oc-deploy.1 @@ -60,6 +60,10 @@ If no options are given, shows information about the latest deployment. \fB\-\-enable\-triggers\fP=false Enables all image triggers for the deployment config. +.PP +\fB\-\-follow\fP=false + Follow the logs of a deployment + .PP \fB\-\-latest\fP=false Start a new deployment now. @@ -146,6 +150,9 @@ If no options are given, shows information about the latest deployment. # Start a new deployment based on the 'database' oc deploy database \-\-latest + # Start a new deployment and follow its log + oc deploy database \-\-latest \-\-follow + # Retry the latest failed deployment based on 'frontend' # The deployer pod and any hook pods are deleted for the latest failed deployment oc deploy frontend \-\-retry diff --git a/docs/man/man1/openshift-cli-deploy.1 b/docs/man/man1/openshift-cli-deploy.1 index a95b3132e4c1..f9f5134d37a6 100644 --- a/docs/man/man1/openshift-cli-deploy.1 +++ b/docs/man/man1/openshift-cli-deploy.1 @@ -60,6 +60,10 @@ If no options are given, shows information about the latest deployment. \fB\-\-enable\-triggers\fP=false Enables all image triggers for the deployment config. +.PP +\fB\-\-follow\fP=false + Follow the logs of a deployment + .PP \fB\-\-latest\fP=false Start a new deployment now. @@ -146,6 +150,9 @@ If no options are given, shows information about the latest deployment. # Start a new deployment based on the 'database' openshift cli deploy database \-\-latest + # Start a new deployment and follow its log + openshift cli deploy database \-\-latest \-\-follow + # Retry the latest failed deployment based on 'frontend' # The deployer pod and any hook pods are deleted for the latest failed deployment openshift cli deploy frontend \-\-retry diff --git a/pkg/cmd/cli/cmd/deploy.go b/pkg/cmd/cli/cmd/deploy.go index 68af72c2b468..f574a47c6db2 100644 --- a/pkg/cmd/cli/cmd/deploy.go +++ b/pkg/cmd/cli/cmd/deploy.go @@ -40,6 +40,7 @@ type DeployOptions struct { retryDeploy bool cancelDeploy bool enableTriggers bool + follow bool } const ( @@ -79,6 +80,9 @@ If no options are given, shows information about the latest deployment.` # Start a new deployment based on the 'database' %[1]s deploy database --latest + # Start a new deployment and follow its log + %[1]s deploy database --latest --follow + # 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 @@ -118,6 +122,7 @@ func NewCmdDeploy(fullName string, f *clientcmd.Factory, out io.Writer) *cobra.C cmd.Flags().BoolVar(&options.retryDeploy, "retry", false, "Retry the latest failed deployment.") cmd.Flags().BoolVar(&options.cancelDeploy, "cancel", false, "Cancel the in-progress deployment.") cmd.Flags().BoolVar(&options.enableTriggers, "enable-triggers", false, "Enables all image triggers for the deployment config.") + cmd.Flags().BoolVar(&options.follow, "follow", false, "Follow the logs of a deployment") return cmd } @@ -161,9 +166,15 @@ func (o DeployOptions) Validate() error { numOptions++ } if o.cancelDeploy { + if o.follow { + return errors.New("cannot follow the logs while canceling a deployment") + } numOptions++ } if o.enableTriggers { + if o.follow { + return errors.New("cannot follow the logs while enabling triggers for a deployment") + } numOptions++ } if numOptions > 1 { @@ -189,14 +200,17 @@ func (o DeployOptions) RunDeploy() error { switch { case o.deployLatest: - err = o.deploy(config, o.out) + err = o.deploy(config) case o.retryDeploy: - err = o.retry(config, o.out) + err = o.retry(config) case o.cancelDeploy: - err = o.cancel(config, o.out) + err = o.cancel(config) case o.enableTriggers: - err = o.reenableTriggers(config, o.out) + err = o.reenableTriggers(config) default: + if o.follow { + return o.getLogs(config) + } describer := describe.NewLatestDeploymentsDescriber(o.osClient, o.kubeClient, -1) desc, err := describer.Describe(config.Namespace, config.Name) if err != nil { @@ -210,7 +224,7 @@ func (o DeployOptions) RunDeploy() error { // deploy launches a new deployment unless there's already a deployment // process in progress for config. -func (o DeployOptions) deploy(config *deployapi.DeploymentConfig, out io.Writer) error { +func (o DeployOptions) deploy(config *deployapi.DeploymentConfig) error { if config.Spec.Paused { return fmt.Errorf("cannot deploy a paused deployment config") } @@ -238,15 +252,18 @@ func (o DeployOptions) deploy(config *deployapi.DeploymentConfig, out io.Writer) if err != nil { return err } - fmt.Fprintf(out, "Started deployment #%d\n", dc.Status.LatestVersion) - fmt.Fprintf(out, "Use '%s logs -f dc/%s' to track its progress.\n", o.baseCommandName, dc.Name) + 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) return nil } // 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. -func (o DeployOptions) retry(config *deployapi.DeploymentConfig, out io.Writer) error { +func (o DeployOptions) retry(config *deployapi.DeploymentConfig) error { if config.Spec.Paused { return fmt.Errorf("cannot retry a paused deployment config") } @@ -295,14 +312,19 @@ func (o DeployOptions) retry(config *deployapi.DeploymentConfig, out io.Writer) delete(deployment.Annotations, deployapi.DeploymentStatusReasonAnnotation) delete(deployment.Annotations, deployapi.DeploymentCancelledAnnotation) _, err = o.kubeClient.ReplicationControllers(deployment.Namespace).Update(deployment) - if err == nil { - fmt.Fprintf(out, "Retried #%d\n", config.Status.LatestVersion) + if err != nil { + return err } - return err + 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 } // cancel cancels any deployment process in progress for config. -func (o DeployOptions) cancel(config *deployapi.DeploymentConfig, out io.Writer) error { +func (o DeployOptions) cancel(config *deployapi.DeploymentConfig) error { if config.Spec.Paused { return fmt.Errorf("cannot cancel a paused deployment config") } @@ -311,7 +333,7 @@ func (o DeployOptions) cancel(config *deployapi.DeploymentConfig, out io.Writer) return err } if len(deployments.Items) == 0 { - fmt.Fprintf(out, "There have been no deployments for %s/%s\n", config.Namespace, config.Name) + 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)) @@ -332,10 +354,10 @@ func (o DeployOptions) cancel(config *deployapi.DeploymentConfig, out io.Writer) deployment.Annotations[deployapi.DeploymentStatusReasonAnnotation] = deployapi.DeploymentCancelledByUser _, err := o.kubeClient.ReplicationControllers(deployment.Namespace).Update(&deployment) if err == nil { - fmt.Fprintf(out, "Cancelled deployment #%d\n", config.Status.LatestVersion) + fmt.Fprintf(o.out, "Cancelled deployment #%d\n", config.Status.LatestVersion) anyCancelled = true } else { - fmt.Fprintf(out, "Couldn't cancel deployment #%d (status: %s): %v\n", deployutil.DeploymentVersionFor(&deployment), status, err) + 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)) } } @@ -350,7 +372,7 @@ func (o DeployOptions) cancel(config *deployapi.DeploymentConfig, out io.Writer) maybeCancelling = " (cancelling)" } timeAt := strings.ToLower(units.HumanDuration(time.Now().Sub(latest.CreationTimestamp.Time))) - fmt.Fprintf(out, "No deployments are in progress (latest deployment #%d %s%s %s ago)\n", + 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, @@ -360,7 +382,7 @@ func (o DeployOptions) cancel(config *deployapi.DeploymentConfig, out io.Writer) } // reenableTriggers enables all image triggers and then persists config. -func (o DeployOptions) reenableTriggers(config *deployapi.DeploymentConfig, out io.Writer) error { +func (o DeployOptions) reenableTriggers(config *deployapi.DeploymentConfig) error { enabled := []string{} for _, trigger := range config.Spec.Triggers { if trigger.Type == deployapi.DeploymentTriggerOnImageChange { @@ -369,13 +391,26 @@ func (o DeployOptions) reenableTriggers(config *deployapi.DeploymentConfig, out } } if len(enabled) == 0 { - fmt.Fprintln(out, "No image triggers found to enable") + fmt.Fprintln(o.out, "No image triggers found to enable") return nil } _, err := o.osClient.DeploymentConfigs(config.Namespace).Update(config) if err != nil { return err } - fmt.Fprintf(out, "Enabled image triggers: %s\n", strings.Join(enabled, ",")) + fmt.Fprintf(o.out, "Enabled image triggers: %s\n", strings.Join(enabled, ",")) return nil } + +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 +} diff --git a/pkg/cmd/cli/cmd/deploy_test.go b/pkg/cmd/cli/cmd/deploy_test.go index 6b018dbb8c6a..ecb96d224fa3 100644 --- a/pkg/cmd/cli/cmd/deploy_test.go +++ b/pkg/cmd/cli/cmd/deploy_test.go @@ -56,8 +56,8 @@ func TestCmdDeploy_latestOk(t *testing.T) { return true, deploymentFor(config, status), nil }) - o := &DeployOptions{osClient: osClient, kubeClient: kubeClient} - err := o.deploy(config, ioutil.Discard) + o := &DeployOptions{osClient: osClient, kubeClient: kubeClient, out: ioutil.Discard} + err := o.deploy(config) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -84,9 +84,9 @@ func TestCmdDeploy_latestConcurrentRejection(t *testing.T) { config := deploytest.OkDeploymentConfig(1) existingDeployment := deploymentFor(config, status) kubeClient := ktc.NewSimpleFake(existingDeployment) - o := &DeployOptions{kubeClient: kubeClient} + o := &DeployOptions{kubeClient: kubeClient, out: ioutil.Discard} - err := o.deploy(config, ioutil.Discard) + err := o.deploy(config) if err == nil { t.Errorf("expected an error starting deployment with existing status %s", status) } @@ -102,8 +102,8 @@ func TestCmdDeploy_latestLookupError(t *testing.T) { }) config := deploytest.OkDeploymentConfig(1) - o := &DeployOptions{kubeClient: kubeClient} - err := o.deploy(config, ioutil.Discard) + o := &DeployOptions{kubeClient: kubeClient, out: ioutil.Discard} + err := o.deploy(config) if err == nil { t.Fatal("expected an error") @@ -150,8 +150,8 @@ func TestCmdDeploy_retryOk(t *testing.T) { return true, nil, nil }) - o := &DeployOptions{kubeClient: kubeClient} - err := o.retry(config, ioutil.Discard) + o := &DeployOptions{kubeClient: kubeClient, out: ioutil.Discard} + err := o.retry(config) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -194,8 +194,8 @@ func TestCmdDeploy_retryRejectNonFailed(t *testing.T) { config := deploytest.OkDeploymentConfig(1) existingDeployment := deploymentFor(config, status) kubeClient := ktc.NewSimpleFake(existingDeployment) - o := &DeployOptions{kubeClient: kubeClient} - err := o.retry(config, ioutil.Discard) + o := &DeployOptions{kubeClient: kubeClient, out: ioutil.Discard} + err := o.retry(config) if err == nil { t.Errorf("expected an error retrying deployment with status %s", status) } @@ -255,9 +255,9 @@ func TestCmdDeploy_cancelOk(t *testing.T) { return true, existingDeployments, nil }) - o := &DeployOptions{kubeClient: kubeClient} + o := &DeployOptions{kubeClient: kubeClient, out: ioutil.Discard} - err := o.cancel(config, ioutil.Discard) + err := o.cancel(config) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -309,8 +309,8 @@ func TestDeploy_reenableTriggers(t *testing.T) { config.Spec.Triggers = append(config.Spec.Triggers, mktrigger()) } - o := &DeployOptions{osClient: osClient} - err := o.reenableTriggers(config, ioutil.Discard) + o := &DeployOptions{osClient: osClient, out: ioutil.Discard} + err := o.reenableTriggers(config) if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/test/extended/deployments/deployments.go b/test/extended/deployments/deployments.go index 6846db0018f3..b6ab200c3ddd 100644 --- a/test/extended/deployments/deployments.go +++ b/test/extended/deployments/deployments.go @@ -236,12 +236,7 @@ var _ = g.Describe("deploymentconfigs", func() { g.By("deploying a few more times") for i := 0; i < 3; i++ { - out, err = oc.Run("deploy").Args("--latest", "deployment-test").Output() - o.Expect(err).NotTo(o.HaveOccurred()) - - o.Expect(waitForLatestCondition(oc, "deployment-test", deploymentRunTimeout, deploymentRunning)).NotTo(o.HaveOccurred()) - - out, err = oc.Run("logs").Args("-f", "dc/deployment-test").Output() + out, err = oc.Run("deploy").Args("--latest", "--follow", "deployment-test").Output() o.Expect(err).NotTo(o.HaveOccurred()) g.By("verifying the deployment is marked complete and scaled to zero") @@ -290,7 +285,7 @@ var _ = g.Describe("deploymentconfigs", func() { o.Expect(waitForLatestCondition(oc, "custom-deployment", deploymentRunTimeout, deploymentRunning)).NotTo(o.HaveOccurred()) - out, err = oc.Run("logs").Args("-f", "dc/custom-deployment").Output() + out, err = oc.Run("deploy").Args("--follow", "dc/custom-deployment").Output() o.Expect(err).NotTo(o.HaveOccurred()) g.By("verifying the deployment is marked complete")