From 1bf99e2e4ecb27d0dd4579c9f137c957ba159901 Mon Sep 17 00:00:00 2001 From: Antoine Pelisse Date: Tue, 2 Apr 2019 17:02:28 -0700 Subject: [PATCH] Create `kubectl rollout restart deployment/$deployment` to do a rolling restart --- pkg/kubectl/cmd/rollout/BUILD | 1 + pkg/kubectl/cmd/rollout/rollout.go | 2 +- pkg/kubectl/cmd/rollout/rollout_restart.go | 187 ++++++++++++++++++ pkg/kubectl/polymorphichelpers/BUILD | 1 + pkg/kubectl/polymorphichelpers/interface.go | 7 + .../polymorphichelpers/objectrestarter.go | 77 ++++++++ test/cmd/apps.sh | 8 + 7 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 pkg/kubectl/cmd/rollout/rollout_restart.go create mode 100644 pkg/kubectl/polymorphichelpers/objectrestarter.go diff --git a/pkg/kubectl/cmd/rollout/BUILD b/pkg/kubectl/cmd/rollout/BUILD index 9d56043dfbb..702fba99d5f 100644 --- a/pkg/kubectl/cmd/rollout/BUILD +++ b/pkg/kubectl/cmd/rollout/BUILD @@ -10,6 +10,7 @@ go_library( "rollout.go", "rollout_history.go", "rollout_pause.go", + "rollout_restart.go", "rollout_resume.go", "rollout_status.go", "rollout_undo.go", diff --git a/pkg/kubectl/cmd/rollout/rollout.go b/pkg/kubectl/cmd/rollout/rollout.go index f5d172a404d..f8c01839d46 100644 --- a/pkg/kubectl/cmd/rollout/rollout.go +++ b/pkg/kubectl/cmd/rollout/rollout.go @@ -19,7 +19,6 @@ package rollout import ( "github.com/lithammer/dedent" "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" "k8s.io/kubernetes/pkg/kubectl/util/i18n" @@ -62,6 +61,7 @@ func NewCmdRollout(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobr cmd.AddCommand(NewCmdRolloutResume(f, streams)) cmd.AddCommand(NewCmdRolloutUndo(f, streams)) cmd.AddCommand(NewCmdRolloutStatus(f, streams)) + cmd.AddCommand(NewCmdRolloutRestart(f, streams)) return cmd } diff --git a/pkg/kubectl/cmd/rollout/rollout_restart.go b/pkg/kubectl/cmd/rollout/rollout_restart.go new file mode 100644 index 00000000000..b273f3336b5 --- /dev/null +++ b/pkg/kubectl/cmd/rollout/rollout_restart.go @@ -0,0 +1,187 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rollout + +import ( + "fmt" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/types" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/kubernetes/pkg/kubectl/cmd/set" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/polymorphichelpers" + "k8s.io/kubernetes/pkg/kubectl/scheme" + "k8s.io/kubernetes/pkg/kubectl/util/i18n" + "k8s.io/kubernetes/pkg/kubectl/util/templates" +) + +// RestartOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of +// referencing the cmd.Flags() +type RestartOptions struct { + PrintFlags *genericclioptions.PrintFlags + ToPrinter func(string) (printers.ResourcePrinter, error) + + Resources []string + + Builder func() *resource.Builder + Restarter polymorphichelpers.ObjectRestarterFunc + Namespace string + EnforceNamespace bool + + resource.FilenameOptions + genericclioptions.IOStreams +} + +var ( + restartLong = templates.LongDesc(` + Restart a resource. + + A deployment with the "RolloutStrategy" will be rolling restarted.`) + + restartExample = templates.Examples(` + # Restart a deployment + kubectl rollout restart deployment/nginx`) +) + +// NewRolloutRestartOptions returns an initialized RestartOptions instance +func NewRolloutRestartOptions(streams genericclioptions.IOStreams) *RestartOptions { + return &RestartOptions{ + PrintFlags: genericclioptions.NewPrintFlags("restarted").WithTypeSetter(scheme.Scheme), + IOStreams: streams, + } +} + +// NewCmdRolloutRestart returns a Command instance for 'rollout restart' sub command +func NewCmdRolloutRestart(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewRolloutRestartOptions(streams) + + validArgs := []string{"deployment"} + + cmd := &cobra.Command{ + Use: "restart RESOURCE", + DisableFlagsInUseLine: true, + Short: i18n.T("Restart a resource"), + Long: restartLong, + Example: restartExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(f, cmd, args)) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.RunRestart()) + }, + ValidArgs: validArgs, + } + + usage := "identifying the resource to get from a server." + cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) + o.PrintFlags.AddFlags(cmd) + return cmd +} + +// Complete completes all the required options +func (o *RestartOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + o.Resources = args + + o.Restarter = polymorphichelpers.ObjectRestarterFn + + var err error + o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return err + } + + o.ToPrinter = func(operation string) (printers.ResourcePrinter, error) { + o.PrintFlags.NamePrintFlags.Operation = operation + return o.PrintFlags.ToPrinter() + } + + o.Builder = f.NewBuilder + + return nil +} + +func (o *RestartOptions) Validate() error { + if len(o.Resources) == 0 && cmdutil.IsFilenameSliceEmpty(o.Filenames, o.Kustomize) { + return fmt.Errorf("required resource not specified") + } + return nil +} + +// RunRestart performs the execution of 'rollout restart' sub command +func (o RestartOptions) RunRestart() error { + r := o.Builder(). + WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...). + NamespaceParam(o.Namespace).DefaultNamespace(). + FilenameParam(o.EnforceNamespace, &o.FilenameOptions). + ResourceTypeOrNameArgs(true, o.Resources...). + ContinueOnError(). + Latest(). + Flatten(). + Do() + if err := r.Err(); err != nil { + return err + } + + allErrs := []error{} + infos, err := r.Infos() + if err != nil { + // restore previous command behavior where + // an error caused by retrieving infos due to + // at least a single broken object did not result + // in an immediate return, but rather an overall + // aggregation of errors. + allErrs = append(allErrs, err) + } + + for _, patch := range set.CalculatePatches(infos, scheme.DefaultJSONEncoder(), set.PatchFn(o.Restarter)) { + info := patch.Info + + if patch.Err != nil { + resourceString := info.Mapping.Resource.Resource + if len(info.Mapping.Resource.Group) > 0 { + resourceString = resourceString + "." + info.Mapping.Resource.Group + } + allErrs = append(allErrs, fmt.Errorf("error: %s %q %v", resourceString, info.Name, patch.Err)) + continue + } + + if string(patch.Patch) == "{}" || len(patch.Patch) == 0 { + allErrs = append(allErrs, fmt.Errorf("failed to create patch for %v: empty patch", info.Name)) + } + + obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, types.StrategicMergePatchType, patch.Patch, nil) + if err != nil { + allErrs = append(allErrs, fmt.Errorf("failed to patch: %v", err)) + continue + } + + info.Refresh(obj, true) + printer, err := o.ToPrinter("restarted") + if err != nil { + allErrs = append(allErrs, err) + continue + } + if err = printer.PrintObj(info.Object, o.Out); err != nil { + allErrs = append(allErrs, err) + } + } + + return utilerrors.NewAggregate(allErrs) +} diff --git a/pkg/kubectl/polymorphichelpers/BUILD b/pkg/kubectl/polymorphichelpers/BUILD index cbf43e685c2..70ff133c0c9 100644 --- a/pkg/kubectl/polymorphichelpers/BUILD +++ b/pkg/kubectl/polymorphichelpers/BUILD @@ -11,6 +11,7 @@ go_library( "logsforobject.go", "mapbasedselectorforobject.go", "objectpauser.go", + "objectrestarter.go", "objectresumer.go", "portsforobject.go", "protocolsforobject.go", diff --git a/pkg/kubectl/polymorphichelpers/interface.go b/pkg/kubectl/polymorphichelpers/interface.go index 46f3b5a15a5..808ffe14d64 100644 --- a/pkg/kubectl/polymorphichelpers/interface.go +++ b/pkg/kubectl/polymorphichelpers/interface.go @@ -106,3 +106,10 @@ type RollbackerFunc func(restClientGetter genericclioptions.RESTClientGetter, ma // RollbackerFn gives a way to easily override the function for unit testing if needed var RollbackerFn RollbackerFunc = rollbacker + +// ObjectRestarterFunc is a function type that updates an annotation in a deployment to restart it.. +type ObjectRestarterFunc func(runtime.Object) ([]byte, error) + +// ObjectRestarterFn gives a way to easily override the function for unit testing if needed. +// Returns the patched object in bytes and any error that occurred during the encoding. +var ObjectRestarterFn ObjectRestarterFunc = defaultObjectRestarter diff --git a/pkg/kubectl/polymorphichelpers/objectrestarter.go b/pkg/kubectl/polymorphichelpers/objectrestarter.go new file mode 100644 index 00000000000..1110beb7f36 --- /dev/null +++ b/pkg/kubectl/polymorphichelpers/objectrestarter.go @@ -0,0 +1,77 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package polymorphichelpers + +import ( + "errors" + "fmt" + "time" + + appsv1 "k8s.io/api/apps/v1" + appsv1beta1 "k8s.io/api/apps/v1beta1" + appsv1beta2 "k8s.io/api/apps/v1beta2" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/kubernetes/pkg/kubectl/scheme" +) + +func defaultObjectRestarter(obj runtime.Object) ([]byte, error) { + switch obj := obj.(type) { + case *extensionsv1beta1.Deployment: + if obj.Spec.Paused { + return nil, errors.New("can't restart paused deployment (run rollout resume first)") + } + if obj.Spec.Template.ObjectMeta.Annotations == nil { + obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + return runtime.Encode(scheme.Codecs.LegacyCodec(extensionsv1beta1.SchemeGroupVersion), obj) + + case *appsv1.Deployment: + if obj.Spec.Paused { + return nil, errors.New("can't restart paused deployment (run rollout resume first)") + } + if obj.Spec.Template.ObjectMeta.Annotations == nil { + obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), obj) + + case *appsv1beta2.Deployment: + if obj.Spec.Paused { + return nil, errors.New("can't restart paused deployment (run rollout resume first)") + } + if obj.Spec.Template.ObjectMeta.Annotations == nil { + obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta2.SchemeGroupVersion), obj) + + case *appsv1beta1.Deployment: + if obj.Spec.Paused { + return nil, errors.New("can't restart paused deployment (run rollout resume first)") + } + if obj.Spec.Template.ObjectMeta.Annotations == nil { + obj.Spec.Template.ObjectMeta.Annotations = make(map[string]string) + } + obj.Spec.Template.ObjectMeta.Annotations["kubectl.kubernetes.io/restartedAt"] = time.Now().Format(time.RFC3339) + return runtime.Encode(scheme.Codecs.LegacyCodec(appsv1beta1.SchemeGroupVersion), obj) + + default: + return nil, fmt.Errorf("restarting is not supported") + } +} diff --git a/test/cmd/apps.sh b/test/cmd/apps.sh index 4c50a20bc83..1f947d2e59d 100755 --- a/test/cmd/apps.sh +++ b/test/cmd/apps.sh @@ -309,6 +309,8 @@ run_deployment_tests() { kubectl-with-retry rollout pause deployment nginx "${kube_flags[@]}" # A paused deployment cannot be rolled back ! kubectl rollout undo deployment nginx "${kube_flags[@]}" + # A paused deployment cannot be restarted + ! kubectl rollout restart deployment nginx "${kube_flags[@]}" # Resume the deployment kubectl-with-retry rollout resume deployment nginx "${kube_flags[@]}" # The resumed deployment can now be rolled back @@ -318,6 +320,12 @@ run_deployment_tests() { kubectl get rs "${newrs}" -o yaml | grep "deployment.kubernetes.io/revision-history: 1,3" # Check that trying to watch the status of a superseded revision returns an error ! kubectl rollout status deployment/nginx --revision=3 + # Restarting the deployment creates a new replicaset + kubectl rollout restart deployment/nginx + sleep 1 + newrs="$(kubectl describe deployment nginx | grep NewReplicaSet | awk '{print $2}')" + rs="$(kubectl get rs "${newrs}" -o yaml)" + kube::test::if_has_string "${rs}" "deployment.kubernetes.io/revision: \"6\"" cat hack/testdata/deployment-revision1.yaml | ${SED} "s/name: nginx$/name: nginx2/" | kubectl create -f - "${kube_flags[@]}" # Deletion of both deployments should not be blocked kubectl delete deployment nginx2 "${kube_flags[@]}"