diff --git a/hack/make-rules/test-cmd-util.sh b/hack/make-rules/test-cmd-util.sh index 76c1c1fd57..3293e99d0c 100644 --- a/hack/make-rules/test-cmd-util.sh +++ b/hack/make-rules/test-cmd-util.sh @@ -41,6 +41,8 @@ IMAGE_NGINX="gcr.io/google-containers/nginx:1.7.9" IMAGE_DEPLOYMENT_R1="gcr.io/google-containers/nginx:test-cmd" # deployment-revision1.yaml IMAGE_DEPLOYMENT_R2="$IMAGE_NGINX" # deployment-revision2.yaml IMAGE_PERL="gcr.io/google-containers/perl" +IMAGE_DAEMONSET_R1="gcr.io/google-containers/pause:2.0" +IMAGE_DAEMONSET_R2="gcr.io/google-containers/pause:latest" # Expose kubectl directly for readability PATH="${KUBE_OUTPUT_HOSTBIN}":$PATH @@ -71,6 +73,7 @@ subjectaccessreviews="subjectaccessreviews" thirdpartyresources="thirdpartyresources" customresourcedefinitions="customresourcedefinitions" daemonsets="daemonsets" +controllerrevisions="controllerrevisions" # Stops the running kubectl proxy, if there is one. @@ -2868,6 +2871,39 @@ run_daemonset_tests() { kubectl apply -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}" # Template Generation should stay 1 kube::test::get_object_assert 'daemonsets bind' "{{${template_generation_field}}}" '1' + # Clean up + kubectl delete -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}" +} + +run_daemonset_history_tests() { + kube::log::status "Testing kubectl(v1:daemonsets, v1:controllerrevisions)" + + ### Test rolling back a DaemonSet + # Pre-condition: no DaemonSet or its pods exists + kube::test::get_object_assert daemonsets "{{range.items}}{{$id_field}}:{{end}}" '' + # Command + # Create a DaemonSet (revision 1) + kubectl apply -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}" + # Rollback to revision 1 - should be no-op + kubectl rollout undo daemonset --to-revision=1 "${kube_flags[@]}" + kube::test::get_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R1}:" + # Update the DaemonSet (revision 2) + kubectl apply -f hack/testdata/rollingupdate-daemonset-rv2.yaml "${kube_flags[@]}" + kube::test::wait_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R2}:" + # Rollback to revision 1 with dry-run - should be no-op + kubectl rollout undo daemonset --dry-run=true "${kube_flags[@]}" + kube::test::get_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R2}:" + # Rollback to revision 1 + kubectl rollout undo daemonset --to-revision=1 "${kube_flags[@]}" + kube::test::wait_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R1}:" + # Rollback to revision 1000000 - should fail + output_message=$(! kubectl rollout undo daemonset --to-revision=1000000 "${kube_flags[@]}" 2>&1) + kube::test::if_has_string "${output_message}" "unable to find specified revision" + kube::test::get_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R1}:" + # Rollback to last revision + kubectl rollout undo daemonset "${kube_flags[@]}" + kube::test::wait_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R2}:" + # Clean up kubectl delete -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}" } @@ -3103,6 +3139,7 @@ runTests() { pdb_min_available=".spec.minAvailable" pdb_max_unavailable=".spec.maxUnavailable" template_generation_field=".spec.templateGeneration" + daemonset_image_field="(index .spec.template.spec.containers 0).image" # Make sure "default" namespace exists. if kube::test::if_supports_resource "${namespaces}" ; then @@ -3555,6 +3592,9 @@ runTests() { if kube::test::if_supports_resource "${daemonsets}" ; then run_daemonset_tests + if kube::test::if_supports_resource "${controllerrevisions}"; then + run_daemonset_history_tests + fi fi ########################### diff --git a/hack/testdata/rollingupdate-daemonset-rv2.yaml b/hack/testdata/rollingupdate-daemonset-rv2.yaml new file mode 100644 index 0000000000..dd5768d295 --- /dev/null +++ b/hack/testdata/rollingupdate-daemonset-rv2.yaml @@ -0,0 +1,27 @@ +apiVersion: extensions/v1beta1 +kind: DaemonSet +metadata: + name: bind +spec: + updateStrategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 10% + template: + metadata: + labels: + service: bind + spec: + affinity: + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: "service" + operator: "In" + values: ["bind"] + topologyKey: "kubernetes.io/hostname" + namespaces: [] + containers: + - name: kubernetes-pause + image: gcr.io/google-containers/pause:latest diff --git a/pkg/controller/daemon/BUILD b/pkg/controller/daemon/BUILD index 9305e1ac27..def942a003 100644 --- a/pkg/controller/daemon/BUILD +++ b/pkg/controller/daemon/BUILD @@ -48,6 +48,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/intstr:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library", diff --git a/pkg/controller/daemon/update.go b/pkg/controller/daemon/update.go index 4f9eb91ee2..abe8987ce2 100644 --- a/pkg/controller/daemon/update.go +++ b/pkg/controller/daemon/update.go @@ -17,8 +17,6 @@ limitations under the License. package daemon import ( - "bytes" - "encoding/json" "fmt" "sort" @@ -29,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" intstrutil "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/json" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/v1" podutil "k8s.io/kubernetes/pkg/api/v1/pod" @@ -297,23 +296,18 @@ func (dsc *DaemonSetsController) controlledHistories(ds *extensions.DaemonSet) ( // Match check if ds template is semantically equal to the template stored in history func Match(template *v1.PodTemplateSpec, history *apps.ControllerRevision) (bool, error) { - t, err := decodeHistory(history) + t, err := DecodeHistory(history) return apiequality.Semantic.DeepEqual(template, t), err } -func decodeHistory(history *apps.ControllerRevision) (*v1.PodTemplateSpec, error) { - raw := history.Data.Raw - decoder := json.NewDecoder(bytes.NewBuffer(raw)) +func DecodeHistory(history *apps.ControllerRevision) (*v1.PodTemplateSpec, error) { template := v1.PodTemplateSpec{} - err := decoder.Decode(&template) + err := json.Unmarshal(history.Data.Raw, &template) return &template, err } func encodeTemplate(template *v1.PodTemplateSpec) ([]byte, error) { - buffer := new(bytes.Buffer) - encoder := json.NewEncoder(buffer) - err := encoder.Encode(template) - return buffer.Bytes(), err + return json.Marshal(template) } func (dsc *DaemonSetsController) snapshot(ds *extensions.DaemonSet, revision int64) (*apps.ControllerRevision, error) { diff --git a/pkg/kubectl/BUILD b/pkg/kubectl/BUILD index ec5d79ff3f..134093b4fd 100644 --- a/pkg/kubectl/BUILD +++ b/pkg/kubectl/BUILD @@ -66,6 +66,7 @@ go_library( "//pkg/apis/policy:go_default_library", "//pkg/apis/rbac:go_default_library", "//pkg/client/clientset_generated/clientset:go_default_library", + "//pkg/client/clientset_generated/clientset/typed/apps/v1beta1:go_default_library", "//pkg/client/clientset_generated/clientset/typed/core/v1:go_default_library", "//pkg/client/clientset_generated/clientset/typed/extensions/v1beta1:go_default_library", "//pkg/client/clientset_generated/internalclientset:go_default_library", @@ -76,6 +77,7 @@ go_library( "//pkg/client/retry:go_default_library", "//pkg/client/unversioned:go_default_library", "//pkg/controller:go_default_library", + "//pkg/controller/daemon:go_default_library", "//pkg/controller/deployment/util:go_default_library", "//pkg/credentialprovider:go_default_library", "//pkg/kubectl/resource:go_default_library", @@ -88,6 +90,7 @@ go_library( "//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library", "//vendor/github.com/spf13/pflag:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/api/equality:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library", diff --git a/pkg/kubectl/cmd/rollout/rollout.go b/pkg/kubectl/cmd/rollout/rollout.go index 22634f56f3..fd62846ce5 100644 --- a/pkg/kubectl/cmd/rollout/rollout.go +++ b/pkg/kubectl/cmd/rollout/rollout.go @@ -28,15 +28,20 @@ import ( var ( rollout_long = templates.LongDesc(` - Manage a deployment using subcommands like "kubectl rollout undo deployment/abc"`) + Manage the rollout of a resource.` + rollout_valid_resources) rollout_example = templates.Examples(` # Rollback to the previous deployment - kubectl rollout undo deployment/abc`) + kubectl rollout undo deployment/abc + + # Check the rollout status of a daemonset + kubectl rollout status daemonset/foo`) rollout_valid_resources = dedent.Dedent(` Valid resource types include: + * deployments + * daemonsets `) ) @@ -44,7 +49,7 @@ func NewCmdRollout(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "rollout SUBCOMMAND", - Short: i18n.T("Manage a deployment rollout"), + Short: i18n.T("Manage the rollout of a resource"), Long: rollout_long, Example: rollout_example, Run: cmdutil.DefaultSubCommandRun(errOut), @@ -54,7 +59,6 @@ func NewCmdRollout(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { cmd.AddCommand(NewCmdRolloutPause(f, out)) cmd.AddCommand(NewCmdRolloutResume(f, out)) cmd.AddCommand(NewCmdRolloutUndo(f, out)) - cmd.AddCommand(NewCmdRolloutStatus(f, out)) return cmd diff --git a/pkg/kubectl/cmd/rollout/rollout_history.go b/pkg/kubectl/cmd/rollout/rollout_history.go index 7edea7dbbc..8b697d3dc4 100644 --- a/pkg/kubectl/cmd/rollout/rollout_history.go +++ b/pkg/kubectl/cmd/rollout/rollout_history.go @@ -37,14 +37,14 @@ var ( # View the rollout history of a deployment kubectl rollout history deployment/abc - # View the details of deployment revision 3 - kubectl rollout history deployment/abc --revision=3`) + # View the details of daemonset revision 3 + kubectl rollout history daemonset/abc --revision=3`) ) func NewCmdRolloutHistory(f cmdutil.Factory, out io.Writer) *cobra.Command { options := &resource.FilenameOptions{} - validArgs := []string{"deployment"} + validArgs := []string{"deployment", "daemonset"} argAliases := kubectl.ResourceAliases(validArgs) cmd := &cobra.Command{ diff --git a/pkg/kubectl/cmd/rollout/rollout_pause.go b/pkg/kubectl/cmd/rollout/rollout_pause.go index afe2709558..8454e89d7a 100644 --- a/pkg/kubectl/cmd/rollout/rollout_pause.go +++ b/pkg/kubectl/cmd/rollout/rollout_pause.go @@ -53,7 +53,7 @@ var ( Mark the provided resource as paused Paused resources will not be reconciled by a controller. - Use \"kubectl rollout resume\" to resume a paused resource. + Use "kubectl rollout resume" to resume a paused resource. Currently only deployments support being paused.`) pause_example = templates.Examples(` diff --git a/pkg/kubectl/cmd/rollout/rollout_status.go b/pkg/kubectl/cmd/rollout/rollout_status.go index cc9abd32e9..a740da9a0f 100644 --- a/pkg/kubectl/cmd/rollout/rollout_status.go +++ b/pkg/kubectl/cmd/rollout/rollout_status.go @@ -50,7 +50,7 @@ var ( func NewCmdRolloutStatus(f cmdutil.Factory, out io.Writer) *cobra.Command { options := &resource.FilenameOptions{} - validArgs := []string{"deployment"} + validArgs := []string{"deployment", "daemonset"} argAliases := kubectl.ResourceAliases(validArgs) cmd := &cobra.Command{ diff --git a/pkg/kubectl/cmd/rollout/rollout_undo.go b/pkg/kubectl/cmd/rollout/rollout_undo.go index eb60ea492b..678503982e 100644 --- a/pkg/kubectl/cmd/rollout/rollout_undo.go +++ b/pkg/kubectl/cmd/rollout/rollout_undo.go @@ -54,8 +54,8 @@ var ( # Rollback to the previous deployment kubectl rollout undo deployment/abc - # Rollback to deployment revision 3 - kubectl rollout undo deployment/abc --to-revision=3 + # Rollback to daemonset revision 3 + kubectl rollout undo daemonset/abc --to-revision=3 # Rollback to the previous deployment with dry-run kubectl rollout undo --dry-run=true deployment/abc`) @@ -64,7 +64,7 @@ var ( func NewCmdRolloutUndo(f cmdutil.Factory, out io.Writer) *cobra.Command { options := &UndoOptions{} - validArgs := []string{"deployment"} + validArgs := []string{"deployment", "daemonset"} argAliases := kubectl.ResourceAliases(validArgs) cmd := &cobra.Command{ diff --git a/pkg/kubectl/history.go b/pkg/kubectl/history.go index 54ea1e1b27..ab85dad33e 100644 --- a/pkg/kubectl/history.go +++ b/pkg/kubectl/history.go @@ -29,8 +29,11 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/apis/apps" + appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1" "k8s.io/kubernetes/pkg/apis/extensions" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + "k8s.io/kubernetes/pkg/controller" + "k8s.io/kubernetes/pkg/controller/daemon" deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util" printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" sliceutil "k8s.io/kubernetes/pkg/util/slice" @@ -49,6 +52,8 @@ func HistoryViewerFor(kind schema.GroupKind, c clientset.Interface) (HistoryView switch kind { case extensions.Kind("Deployment"), apps.Kind("Deployment"): return &DeploymentHistoryViewer{c}, nil + case extensions.Kind("DaemonSet"): + return &DaemonSetHistoryViewer{c}, nil } return nil, fmt.Errorf("no history viewer has been implemented for %q", kind) } @@ -100,14 +105,7 @@ func (h *DeploymentHistoryViewer) ViewHistory(namespace, name string, revision i if !ok { return "", fmt.Errorf("unable to find the specified revision") } - buf := bytes.NewBuffer([]byte{}) - internalTemplate := &api.PodTemplateSpec{} - if err := v1.Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(template, internalTemplate, nil); err != nil { - return "", fmt.Errorf("failed to convert podtemplate, %v", err) - } - w := printersinternal.NewPrefixWriter(buf) - printersinternal.DescribePodTemplate(internalTemplate, w) - return buf.String(), nil + return printTemplate(template) } // Sort the revisionToChangeCause map by revision @@ -131,6 +129,101 @@ func (h *DeploymentHistoryViewer) ViewHistory(namespace, name string, revision i }) } +func printTemplate(template *v1.PodTemplateSpec) (string, error) { + buf := bytes.NewBuffer([]byte{}) + internalTemplate := &api.PodTemplateSpec{} + if err := v1.Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(template, internalTemplate, nil); err != nil { + return "", fmt.Errorf("failed to convert podtemplate, %v", err) + } + w := printersinternal.NewPrefixWriter(buf) + printersinternal.DescribePodTemplate(internalTemplate, w) + return buf.String(), nil +} + +type DaemonSetHistoryViewer struct { + c clientset.Interface +} + +// ViewHistory returns a revision-to-history map as the revision history of a deployment +// TODO: this should be a describer +func (h *DaemonSetHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) { + ds, err := h.c.Extensions().DaemonSets(namespace).Get(name, metav1.GetOptions{}) + if err != nil { + return "", fmt.Errorf("failed to retrieve DaemonSet %s: %v", name, err) + } + allHistory, err := controlledHistories(h.c, ds) + if err != nil { + return "", fmt.Errorf("unable to find history controlled by DaemonSet %s: %v", ds.Name, err) + } + historyInfo := make(map[int64]*appsv1beta1.ControllerRevision) + for _, history := range allHistory { + // TODO: for now we assume revisions don't overlap, we may need to handle it + historyInfo[history.Revision] = history + } + + if len(historyInfo) == 0 { + return "No rollout history found.", nil + } + + // Print details of a specific revision + if revision > 0 { + history, ok := historyInfo[revision] + if !ok { + return "", fmt.Errorf("unable to find the specified revision") + } + template, err := daemon.DecodeHistory(history) + if err != nil { + return "", fmt.Errorf("unable to decode history %s", history.Name) + } + return printTemplate(template) + } + + // Print an overview of all Revisions + // Sort the revisionToChangeCause map by revision + revisions := make([]int64, 0, len(historyInfo)) + for r := range historyInfo { + revisions = append(revisions, r) + } + sliceutil.SortInts64(revisions) + + return tabbedString(func(out io.Writer) error { + fmt.Fprintf(out, "REVISION\tCHANGE-CAUSE\n") + for _, r := range revisions { + // Find the change-cause of revision r + changeCause := historyInfo[r].Annotations[ChangeCauseAnnotation] + if len(changeCause) == 0 { + changeCause = "" + } + fmt.Fprintf(out, "%d\t%s\n", r, changeCause) + } + return nil + }) +} + +// controlledHistories returns all ControllerRevisions controlled by the given DaemonSet +// TODO: Use external version DaemonSet instead when #3955 is fixed +func controlledHistories(c clientset.Interface, ds *extensions.DaemonSet) ([]*appsv1beta1.ControllerRevision, error) { + var result []*appsv1beta1.ControllerRevision + selector, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector) + if err != nil { + return nil, err + } + versionedClient := versionedClientsetForDaemonSet(c) + historyList, err := versionedClient.AppsV1beta1().ControllerRevisions(ds.Namespace).List(metav1.ListOptions{LabelSelector: selector.String()}) + if err != nil { + return nil, err + } + for i := range historyList.Items { + history := historyList.Items[i] + // Skip history that doesn't belong to the DaemonSet + if controllerRef := controller.GetControllerOf(&history); controllerRef == nil || controllerRef.UID != ds.UID { + continue + } + result = append(result, &history) + } + return result, nil +} + // TODO: copied here until this becomes a describer func tabbedString(f func(io.Writer) error) (string, error) { out := new(tabwriter.Writer) diff --git a/pkg/kubectl/rollback.go b/pkg/kubectl/rollback.go index 95b6fc0e27..b132655193 100644 --- a/pkg/kubectl/rollback.go +++ b/pkg/kubectl/rollback.go @@ -21,8 +21,10 @@ import ( "fmt" "os" "os/signal" + "sort" "syscall" + apiequality "k8s.io/apimachinery/pkg/api/equality" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -30,14 +32,22 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/apis/apps" + appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1" "k8s.io/kubernetes/pkg/apis/extensions" externalextensions "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" + "k8s.io/kubernetes/pkg/client/retry" + "k8s.io/kubernetes/pkg/controller/daemon" deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util" printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" sliceutil "k8s.io/kubernetes/pkg/util/slice" ) +const ( + rollbackSuccess = "rolled back" + rollbackSkipped = "skipped rollback" +) + // Rollbacker provides an interface for resources that can be rolled back. type Rollbacker interface { Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRun bool) (string, error) @@ -47,6 +57,8 @@ func RollbackerFor(kind schema.GroupKind, c clientset.Interface) (Rollbacker, er switch kind { case extensions.Kind("Deployment"), apps.Kind("Deployment"): return &DeploymentRollbacker{c}, nil + case extensions.Kind("DaemonSet"): + return &DaemonSetRollbacker{c}, nil } return nil, fmt.Errorf("no rollbacker has been implemented for %q", kind) } @@ -126,9 +138,9 @@ func isRollbackEvent(e *api.Event) (bool, string) { for _, reason := range rollbackEventReasons { if e.Reason == reason { if reason == deploymentutil.RollbackDone { - return true, "rolled back" + return true, rollbackSuccess } - return true, fmt.Sprintf("skipped rollback (%s: %s)", e.Reason, e.Message) + return true, fmt.Sprintf("%s (%s: %s)", rollbackSkipped, e.Reason, e.Message) } } return false, "" @@ -165,7 +177,7 @@ func simpleDryRun(deployment *extensions.Deployment, c clientset.Interface, toRe if toRevision > 0 { template, ok := revisionToSpec[toRevision] if !ok { - return "", fmt.Errorf("unable to find specified revision") + return "", revisionNotFoundErr(toRevision) } buf := bytes.NewBuffer([]byte{}) internalTemplate := &api.PodTemplateSpec{} @@ -195,3 +207,108 @@ func simpleDryRun(deployment *extensions.Deployment, c clientset.Interface, toRe printersinternal.DescribePodTemplate(internalTemplate, w) return buf.String(), nil } + +type DaemonSetRollbacker struct { + c clientset.Interface +} + +func (r *DaemonSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRun bool) (string, error) { + if toRevision < 0 { + return "", revisionNotFoundErr(toRevision) + } + + ds, ok := obj.(*extensions.DaemonSet) + if !ok { + return "", fmt.Errorf("passed object is not a DaemonSet: %#v", obj) + } + allHistory, err := controlledHistories(r.c, ds) + if err != nil { + return "", fmt.Errorf("unable to find history controlled by DaemonSet %s: %v", ds.Name, err) + } + + if toRevision == 0 && len(allHistory) <= 1 { + return "", fmt.Errorf("no last revision to roll back to") + } + + // Find the history to rollback to + var toHistory *appsv1beta1.ControllerRevision + if toRevision == 0 { + // If toRevision == 0, find the latest revision (2nd max) + sort.Sort(historiesByRevision(allHistory)) + toHistory = allHistory[len(allHistory)-2] + } else { + for _, h := range allHistory { + if h.Revision == toRevision { + // If toRevision != 0, find the history with matching revision + toHistory = h + break + } + } + } + if toHistory == nil { + return "", revisionNotFoundErr(toRevision) + } + + // Get the template of the history to rollback to + toTemplate, err := getInternalTemplate(toHistory) + if err != nil { + return "", err + } + + if dryRun { + content := bytes.NewBuffer([]byte{}) + w := printersinternal.NewPrefixWriter(content) + printersinternal.DescribePodTemplate(toTemplate, w) + return fmt.Sprintf("will roll back to %s", content.String()), nil + } + + // Update DaemonSet template, and retry on conflict + skipUpdate := false + retryErr := retry.RetryOnConflict(retry.DefaultBackoff, func() error { + var err error + ds, err = r.c.Extensions().DaemonSets(ds.Namespace).Get(ds.Name, metav1.GetOptions{}) + if err != nil { + return err + } + if apiequality.Semantic.DeepEqual(toTemplate, &ds.Spec.Template) { + skipUpdate = true + return nil + } + ds.Spec.Template = *toTemplate + _, err = r.c.Extensions().DaemonSets(ds.Namespace).Update(ds) + return err + }) + if retryErr != nil { + return "", retryErr + } + if skipUpdate { + return fmt.Sprintf("%s (current template already matches revision %d)", rollbackSkipped, toRevision), nil + } + + return rollbackSuccess, nil +} + +func getInternalTemplate(toHistory *appsv1beta1.ControllerRevision) (*api.PodTemplateSpec, error) { + template, err := daemon.DecodeHistory(toHistory) + if err != nil { + return nil, err + } + internalTemplate := &api.PodTemplateSpec{} + if err := v1.Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(template, internalTemplate, nil); err != nil { + return nil, fmt.Errorf("failed to convert podtemplate, %v", err) + } + return internalTemplate, nil +} + +func revisionNotFoundErr(r int64) error { + return fmt.Errorf("unable to find specified revision %v in history", r) +} + +// TODO: copied from daemon controller, should extract to a library +type historiesByRevision []*appsv1beta1.ControllerRevision + +func (h historiesByRevision) Len() int { return len(h) } +func (h historiesByRevision) Swap(i, j int) { h[i], h[j] = h[j], h[i] } +func (h historiesByRevision) Less(i, j int) bool { + return h[i].Revision < h[j].Revision +} diff --git a/pkg/kubectl/versioned_client.go b/pkg/kubectl/versioned_client.go index 705516f53c..d1b0481c40 100644 --- a/pkg/kubectl/versioned_client.go +++ b/pkg/kubectl/versioned_client.go @@ -18,6 +18,7 @@ package kubectl import ( externalclientset "k8s.io/kubernetes/pkg/client/clientset_generated/clientset" + apps "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/apps/v1beta1" core "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/core/v1" extensions "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/extensions/v1beta1" internalclientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" @@ -32,3 +33,12 @@ func versionedClientsetForDeployment(internalClient internalclientset.Interface) ExtensionsV1beta1Client: extensions.New(internalClient.Extensions().RESTClient()), } } + +func versionedClientsetForDaemonSet(internalClient internalclientset.Interface) externalclientset.Interface { + if internalClient == nil { + return &externalclientset.Clientset{} + } + return &externalclientset.Clientset{ + AppsV1beta1Client: apps.New(internalClient.Apps().RESTClient()), + } +}