From 1a1e8344a510f5287b07677a1fafd39352c18feb Mon Sep 17 00:00:00 2001 From: Clayton Coleman Date: Thu, 26 Apr 2018 00:18:15 -0400 Subject: [PATCH] kubectl should not have a direct code dependency on controllers The dependency linkage brings in the scheduler and a lot of unrelated code. Instead, selectively copy methods that operate on public APIs. --- pkg/kubectl/BUILD | 2 - pkg/kubectl/rollback.go | 97 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/pkg/kubectl/BUILD b/pkg/kubectl/BUILD index f4de498d53..e20cb894a8 100644 --- a/pkg/kubectl/BUILD +++ b/pkg/kubectl/BUILD @@ -132,9 +132,7 @@ go_library( "//pkg/client/clientset_generated/internalclientset/typed/batch/internalversion:go_default_library", "//pkg/client/clientset_generated/internalclientset/typed/core/internalversion:go_default_library", "//pkg/client/clientset_generated/internalclientset/typed/extensions/internalversion:go_default_library", - "//pkg/controller/daemon:go_default_library", "//pkg/controller/deployment/util:go_default_library", - "//pkg/controller/statefulset:go_default_library", "//pkg/credentialprovider:go_default_library", "//pkg/kubectl/apps:go_default_library", "//pkg/kubectl/cmd/scalejob:go_default_library", diff --git a/pkg/kubectl/rollback.go b/pkg/kubectl/rollback.go index 10ede4b53d..10ac083beb 100644 --- a/pkg/kubectl/rollback.go +++ b/pkg/kubectl/rollback.go @@ -32,18 +32,19 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/json" + "k8s.io/apimachinery/pkg/util/strategicpatch" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/kubernetes" "k8s.io/kubernetes/pkg/api/legacyscheme" api "k8s.io/kubernetes/pkg/apis/core" apiv1 "k8s.io/kubernetes/pkg/apis/core/v1" "k8s.io/kubernetes/pkg/apis/extensions" - "k8s.io/kubernetes/pkg/controller/daemon" - deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util" - "k8s.io/kubernetes/pkg/controller/statefulset" kapps "k8s.io/kubernetes/pkg/kubectl/apps" sliceutil "k8s.io/kubernetes/pkg/kubectl/util/slice" printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" + // kubectl should not be taking dependencies on logic in the controllers + deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util" ) const ( @@ -278,7 +279,7 @@ func (r *DaemonSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations ma } // Skip if the revision already matches current DaemonSet - done, err := daemon.Match(ds, toHistory) + done, err := daemonSetMatch(ds, toHistory) if err != nil { return "", err } @@ -294,6 +295,42 @@ func (r *DaemonSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations ma return rollbackSuccess, nil } +// daemonMatch check if the given DaemonSet's template matches the template stored in the given history. +func daemonSetMatch(ds *appsv1.DaemonSet, history *appsv1.ControllerRevision) (bool, error) { + patch, err := getDaemonSetPatch(ds) + if err != nil { + return false, err + } + return bytes.Equal(patch, history.Data.Raw), nil +} + +// getPatch returns a strategic merge patch that can be applied to restore a Daemonset to a +// previous version. If the returned error is nil the patch is valid. The current state that we save is just the +// PodSpecTemplate. We can modify this later to encompass more state (or less) and remain compatible with previously +// recorded patches. +func getDaemonSetPatch(ds *appsv1.DaemonSet) ([]byte, error) { + dsBytes, err := json.Marshal(ds) + if err != nil { + return nil, err + } + var raw map[string]interface{} + err = json.Unmarshal(dsBytes, &raw) + if err != nil { + return nil, err + } + objCopy := make(map[string]interface{}) + specCopy := make(map[string]interface{}) + + // Create a patch of the DaemonSet that replaces spec.template + spec := raw["spec"].(map[string]interface{}) + template := spec["template"].(map[string]interface{}) + specCopy["template"] = template + template["$patch"] = "replace" + objCopy["spec"] = specCopy + patch, err := json.Marshal(objCopy) + return patch, err +} + type StatefulSetRollbacker struct { c kubernetes.Interface } @@ -321,7 +358,7 @@ func (r *StatefulSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations } if dryRun { - appliedSS, err := statefulset.ApplyRevision(sts, toHistory) + appliedSS, err := applyRevision(sts, toHistory) if err != nil { return "", err } @@ -329,7 +366,7 @@ func (r *StatefulSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations } // Skip if the revision already matches current StatefulSet - done, err := statefulset.Match(sts, toHistory) + done, err := statefulsetMatch(sts, toHistory) if err != nil { return "", err } @@ -345,6 +382,54 @@ func (r *StatefulSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations return rollbackSuccess, nil } +var appsCodec = legacyscheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion) + +// applyRevision returns a new StatefulSet constructed by restoring the state in revision to set. If the returned error +// is nil, the returned StatefulSet is valid. +func applyRevision(set *appsv1.StatefulSet, revision *appsv1.ControllerRevision) (*appsv1.StatefulSet, error) { + clone := set.DeepCopy() + patched, err := strategicpatch.StrategicMergePatch([]byte(runtime.EncodeOrDie(appsCodec, clone)), revision.Data.Raw, clone) + if err != nil { + return nil, err + } + err = json.Unmarshal(patched, clone) + if err != nil { + return nil, err + } + return clone, nil +} + +// statefulsetMatch check if the given StatefulSet's template matches the template stored in the given history. +func statefulsetMatch(ss *appsv1.StatefulSet, history *appsv1.ControllerRevision) (bool, error) { + patch, err := getStatefulSetPatch(ss) + if err != nil { + return false, err + } + return bytes.Equal(patch, history.Data.Raw), nil +} + +// getStatefulSetPatch returns a strategic merge patch that can be applied to restore a StatefulSet to a +// previous version. If the returned error is nil the patch is valid. The current state that we save is just the +// PodSpecTemplate. We can modify this later to encompass more state (or less) and remain compatible with previously +// recorded patches. +func getStatefulSetPatch(set *appsv1.StatefulSet) ([]byte, error) { + str, err := runtime.Encode(appsCodec, set) + if err != nil { + return nil, err + } + var raw map[string]interface{} + json.Unmarshal([]byte(str), &raw) + objCopy := make(map[string]interface{}) + specCopy := make(map[string]interface{}) + spec := raw["spec"].(map[string]interface{}) + template := spec["template"].(map[string]interface{}) + specCopy["template"] = template + template["$patch"] = "replace" + objCopy["spec"] = specCopy + patch, err := json.Marshal(objCopy) + return patch, err +} + // findHistory returns a controllerrevision of a specific revision from the given controllerrevisions. // It returns nil if no such controllerrevision exists. // If toRevision is 0, the last previously used history is returned.