Merge pull request #47075 from janetkuo/ds-history-patch

Automatic merge from submit-queue

Change what is stored in DaemonSet history `.data`

**What this PR does / why we need it**: 
In DaemonSet history `.data`, store a strategic merge patch that can be applied to restore a DaemonSet. Only PodSpecTemplate is saved. 

This will become consistent with the data stored in StatefulSet history. 

Before this fix, a serialized pod template is stored in `.data`; however, seriazlized pod template isn't a `runtime.RawExtension`, and caused problems when controllers try to patch the history's controller ref. 

**Which issue this PR fixes** *(optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)` format, will close that issue when PR gets merged)*: fixes #47008

**Special notes for your reviewer**: @kubernetes/sig-apps-bugs @erictune @kow3ns @kargakis @lukaszo @mengqiy 

**Release note**:

```release-note
NONE
```
pull/6/head
Kubernetes Submit Queue 2017-06-12 23:31:08 -07:00 committed by GitHub
commit aa35738a21
9 changed files with 131 additions and 90 deletions

View File

@ -43,6 +43,7 @@ IMAGE_DEPLOYMENT_R2="$IMAGE_NGINX" # deployment-revision2.yaml
IMAGE_PERL="gcr.io/google-containers/perl" IMAGE_PERL="gcr.io/google-containers/perl"
IMAGE_DAEMONSET_R1="gcr.io/google-containers/pause:2.0" IMAGE_DAEMONSET_R1="gcr.io/google-containers/pause:2.0"
IMAGE_DAEMONSET_R2="gcr.io/google-containers/pause:latest" IMAGE_DAEMONSET_R2="gcr.io/google-containers/pause:latest"
IMAGE_DAEMONSET_R2_2="gcr.io/google-containers/nginx:test-cmd" # rollingupdate-daemonset-rv2.yaml
# Expose kubectl directly for readability # Expose kubectl directly for readability
PATH="${KUBE_OUTPUT_HOSTBIN}":$PATH PATH="${KUBE_OUTPUT_HOSTBIN}":$PATH
@ -2900,23 +2901,32 @@ run_daemonset_history_tests() {
kubectl apply -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}" kubectl apply -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}"
# Rollback to revision 1 - should be no-op # Rollback to revision 1 - should be no-op
kubectl rollout undo daemonset --to-revision=1 "${kube_flags[@]}" kubectl rollout undo daemonset --to-revision=1 "${kube_flags[@]}"
kube::test::get_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R1}:" kube::test::get_object_assert daemonset "{{range.items}}{{$daemonset_image_field0}}:{{end}}" "${IMAGE_DAEMONSET_R1}:"
kube::test::get_object_assert daemonset "{{range.items}}{{$container_len}}{{end}}" "1"
# Update the DaemonSet (revision 2) # Update the DaemonSet (revision 2)
kubectl apply -f hack/testdata/rollingupdate-daemonset-rv2.yaml "${kube_flags[@]}" 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}:" kube::test::wait_object_assert daemonset "{{range.items}}{{$daemonset_image_field0}}:{{end}}" "${IMAGE_DAEMONSET_R2}:"
kube::test::wait_object_assert daemonset "{{range.items}}{{$daemonset_image_field1}}:{{end}}" "${IMAGE_DAEMONSET_R2_2}:"
kube::test::get_object_assert daemonset "{{range.items}}{{$container_len}}{{end}}" "2"
# Rollback to revision 1 with dry-run - should be no-op # Rollback to revision 1 with dry-run - should be no-op
kubectl rollout undo daemonset --dry-run=true "${kube_flags[@]}" kubectl rollout undo daemonset --dry-run=true "${kube_flags[@]}"
kube::test::get_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R2}:" kube::test::get_object_assert daemonset "{{range.items}}{{$daemonset_image_field0}}:{{end}}" "${IMAGE_DAEMONSET_R2}:"
kube::test::get_object_assert daemonset "{{range.items}}{{$daemonset_image_field1}}:{{end}}" "${IMAGE_DAEMONSET_R2_2}:"
kube::test::get_object_assert daemonset "{{range.items}}{{$container_len}}{{end}}" "2"
# Rollback to revision 1 # Rollback to revision 1
kubectl rollout undo daemonset --to-revision=1 "${kube_flags[@]}" kubectl rollout undo daemonset --to-revision=1 "${kube_flags[@]}"
kube::test::wait_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R1}:" kube::test::wait_object_assert daemonset "{{range.items}}{{$daemonset_image_field0}}:{{end}}" "${IMAGE_DAEMONSET_R1}:"
kube::test::get_object_assert daemonset "{{range.items}}{{$container_len}}{{end}}" "1"
# Rollback to revision 1000000 - should fail # Rollback to revision 1000000 - should fail
output_message=$(! kubectl rollout undo daemonset --to-revision=1000000 "${kube_flags[@]}" 2>&1) 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::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}:" kube::test::get_object_assert daemonset "{{range.items}}{{$daemonset_image_field0}}:{{end}}" "${IMAGE_DAEMONSET_R1}:"
kube::test::get_object_assert daemonset "{{range.items}}{{$container_len}}{{end}}" "1"
# Rollback to last revision # Rollback to last revision
kubectl rollout undo daemonset "${kube_flags[@]}" kubectl rollout undo daemonset "${kube_flags[@]}"
kube::test::wait_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R2}:" kube::test::wait_object_assert daemonset "{{range.items}}{{$daemonset_image_field0}}:{{end}}" "${IMAGE_DAEMONSET_R2}:"
kube::test::wait_object_assert daemonset "{{range.items}}{{$daemonset_image_field1}}:{{end}}" "${IMAGE_DAEMONSET_R2_2}:"
kube::test::get_object_assert daemonset "{{range.items}}{{$container_len}}{{end}}" "2"
# Clean up # Clean up
kubectl delete -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}" kubectl delete -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}"
} }
@ -3153,7 +3163,9 @@ runTests() {
pdb_min_available=".spec.minAvailable" pdb_min_available=".spec.minAvailable"
pdb_max_unavailable=".spec.maxUnavailable" pdb_max_unavailable=".spec.maxUnavailable"
template_generation_field=".spec.templateGeneration" template_generation_field=".spec.templateGeneration"
daemonset_image_field="(index .spec.template.spec.containers 0).image" container_len="(len .spec.template.spec.containers)"
daemonset_image_field0="(index .spec.template.spec.containers 0).image"
daemonset_image_field1="(index .spec.template.spec.containers 1).image"
# Make sure "default" namespace exists. # Make sure "default" namespace exists.
if kube::test::if_supports_resource "${namespaces}" ; then if kube::test::if_supports_resource "${namespaces}" ; then

View File

@ -25,3 +25,5 @@ spec:
containers: containers:
- name: kubernetes-pause - name: kubernetes-pause
image: gcr.io/google-containers/pause:latest image: gcr.io/google-containers/pause:latest
- name: app
image: gcr.io/google-containers/nginx:test-cmd

View File

@ -41,7 +41,6 @@ go_library(
"//plugin/pkg/scheduler/algorithm/predicates:go_default_library", "//plugin/pkg/scheduler/algorithm/predicates:go_default_library",
"//plugin/pkg/scheduler/schedulercache:go_default_library", "//plugin/pkg/scheduler/schedulercache:go_default_library",
"//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/golang/glog: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/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/labels:go_default_library", "//vendor/k8s.io/apimachinery/pkg/labels:go_default_library",

View File

@ -17,11 +17,12 @@ limitations under the License.
package daemon package daemon
import ( import (
"bytes"
"fmt" "fmt"
"sort" "sort"
"github.com/golang/glog" "github.com/golang/glog"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
@ -103,7 +104,7 @@ func (dsc *DaemonSetsController) constructHistory(ds *extensions.DaemonSet) (cur
} }
// Compare histories with ds to separate cur and old history // Compare histories with ds to separate cur and old history
found := false found := false
found, err = Match(&ds.Spec.Template, history) found, err = Match(ds, history)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -294,24 +295,44 @@ func (dsc *DaemonSetsController) controlledHistories(ds *extensions.DaemonSet) (
return result, nil return result, nil
} }
// Match check if ds template is semantically equal to the template stored in history // Match check if the given DaemonSet's template matches the template stored in the given history.
func Match(template *v1.PodTemplateSpec, history *apps.ControllerRevision) (bool, error) { func Match(ds *extensions.DaemonSet, history *apps.ControllerRevision) (bool, error) {
t, err := DecodeHistory(history) patch, err := getPatch(ds)
return apiequality.Semantic.DeepEqual(template, t), err if err != nil {
return false, err
}
return bytes.Equal(patch, history.Data.Raw), nil
} }
func DecodeHistory(history *apps.ControllerRevision) (*v1.PodTemplateSpec, error) { // getPatch returns a strategic merge patch that can be applied to restore a Daemonset to a
template := v1.PodTemplateSpec{} // previous version. If the returned error is nil the patch is valid. The current state that we save is just the
err := json.Unmarshal(history.Data.Raw, &template) // PodSpecTemplate. We can modify this later to encompass more state (or less) and remain compatible with previously
return &template, err // recorded patches.
} func getPatch(ds *extensions.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{})
func encodeTemplate(template *v1.PodTemplateSpec) ([]byte, error) { // Create a patch of the DaemonSet that replaces spec.template
return json.Marshal(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
} }
func (dsc *DaemonSetsController) snapshot(ds *extensions.DaemonSet, revision int64) (*apps.ControllerRevision, error) { func (dsc *DaemonSetsController) snapshot(ds *extensions.DaemonSet, revision int64) (*apps.ControllerRevision, error) {
encodedTemplate, err := encodeTemplate(&ds.Spec.Template) patch, err := getPatch(ds)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -325,7 +346,7 @@ func (dsc *DaemonSetsController) snapshot(ds *extensions.DaemonSet, revision int
Annotations: ds.Annotations, Annotations: ds.Annotations,
OwnerReferences: []metav1.OwnerReference{*newControllerRef(ds)}, OwnerReferences: []metav1.OwnerReference{*newControllerRef(ds)},
}, },
Data: runtime.RawExtension{Raw: encodedTemplate}, Data: runtime.RawExtension{Raw: patch},
Revision: revision, Revision: revision,
} }
@ -337,7 +358,7 @@ func (dsc *DaemonSetsController) snapshot(ds *extensions.DaemonSet, revision int
return nil, getErr return nil, getErr
} }
// Check if we already created it // Check if we already created it
done, err := Match(&ds.Spec.Template, existedHistory) done, err := Match(ds, existedHistory)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -154,7 +154,6 @@ go_library(
"//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/spf13/cobra:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library",
"//vendor/github.com/spf13/pflag: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/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/resource:go_default_library",
@ -167,7 +166,9 @@ go_library(
"//vendor/k8s.io/apimachinery/pkg/types:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/errors: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/intstr:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/json:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/sets:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/strategicpatch:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/uuid:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/uuid:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/validation:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",

View File

@ -26,14 +26,17 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/strategicpatch"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/apps"
appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1" appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1"
"k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/apis/extensions"
extensionsv1beta1 "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
externalclientset "k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/controller" "k8s.io/kubernetes/pkg/controller"
"k8s.io/kubernetes/pkg/controller/daemon"
deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util" deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util"
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
sliceutil "k8s.io/kubernetes/pkg/util/slice" sliceutil "k8s.io/kubernetes/pkg/util/slice"
@ -149,13 +152,10 @@ type DaemonSetHistoryViewer struct {
// ViewHistory returns a revision-to-history map as the revision history of a deployment // ViewHistory returns a revision-to-history map as the revision history of a deployment
// TODO: this should be a describer // TODO: this should be a describer
func (h *DaemonSetHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) { func (h *DaemonSetHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
ds, err := h.c.Extensions().DaemonSets(namespace).Get(name, metav1.GetOptions{}) versionedClient := versionedClientsetForDaemonSet(h.c)
ds, allHistory, err := controlledHistories(versionedClient, namespace, name)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to retrieve DaemonSet %s: %v", name, err) return "", fmt.Errorf("unable to find history controlled by 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) historyInfo := make(map[int64]*appsv1beta1.ControllerRevision)
for _, history := range allHistory { for _, history := range allHistory {
@ -173,11 +173,11 @@ func (h *DaemonSetHistoryViewer) ViewHistory(namespace, name string, revision in
if !ok { if !ok {
return "", fmt.Errorf("unable to find the specified revision") return "", fmt.Errorf("unable to find the specified revision")
} }
template, err := daemon.DecodeHistory(history) dsOfHistory, err := applyHistory(ds, history)
if err != nil { if err != nil {
return "", fmt.Errorf("unable to decode history %s", history.Name) return "", fmt.Errorf("unable to parse history %s", history.Name)
} }
return printTemplate(template) return printTemplate(&dsOfHistory.Spec.Template)
} }
// Print an overview of all Revisions // Print an overview of all Revisions
@ -255,17 +255,19 @@ func (h *StatefulSetHistoryViewer) ViewHistory(namespace, name string, revision
} }
// controlledHistories returns all ControllerRevisions controlled by the given DaemonSet // controlledHistories returns all ControllerRevisions controlled by the given DaemonSet
// TODO: Use external version DaemonSet instead when #3955 is fixed func controlledHistories(c externalclientset.Interface, namespace, name string) (*extensionsv1beta1.DaemonSet, []*appsv1beta1.ControllerRevision, error) {
func controlledHistories(c clientset.Interface, ds *extensions.DaemonSet) ([]*appsv1beta1.ControllerRevision, error) { ds, err := c.ExtensionsV1beta1().DaemonSets(namespace).Get(name, metav1.GetOptions{})
if err != nil {
return nil, nil, fmt.Errorf("failed to retrieve DaemonSet %s: %v", name, err)
}
var result []*appsv1beta1.ControllerRevision var result []*appsv1beta1.ControllerRevision
selector, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector) selector, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
versionedClient := versionedClientsetForDaemonSet(c) historyList, err := c.AppsV1beta1().ControllerRevisions(ds.Namespace).List(metav1.ListOptions{LabelSelector: selector.String()})
historyList, err := versionedClient.AppsV1beta1().ControllerRevisions(ds.Namespace).List(metav1.ListOptions{LabelSelector: selector.String()})
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
for i := range historyList.Items { for i := range historyList.Items {
history := historyList.Items[i] history := historyList.Items[i]
@ -275,7 +277,29 @@ func controlledHistories(c clientset.Interface, ds *extensions.DaemonSet) ([]*ap
} }
result = append(result, &history) result = append(result, &history)
} }
return result, nil return ds, result, nil
}
// applyHistory returns a specific revision of DaemonSet by applying the given history to a copy of the given DaemonSet
func applyHistory(ds *extensionsv1beta1.DaemonSet, history *appsv1beta1.ControllerRevision) (*extensionsv1beta1.DaemonSet, error) {
obj, err := api.Scheme.New(ds.GroupVersionKind())
if err != nil {
return nil, err
}
clone := obj.(*extensionsv1beta1.DaemonSet)
cloneBytes, err := json.Marshal(clone)
if err != nil {
return nil, err
}
patched, err := strategicpatch.StrategicMergePatch(cloneBytes, history.Data.Raw, clone)
if err != nil {
return nil, err
}
err = json.Unmarshal(patched, clone)
if err != nil {
return nil, err
}
return clone, nil
} }
// TODO: copied here until this becomes a describer // TODO: copied here until this becomes a describer

View File

@ -24,10 +24,10 @@ import (
"sort" "sort"
"syscall" "syscall"
apiequality "k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/api/v1"
@ -36,7 +36,6 @@ import (
"k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/apis/extensions"
externalextensions "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" externalextensions "k8s.io/kubernetes/pkg/apis/extensions/v1beta1"
clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
"k8s.io/kubernetes/pkg/client/retry"
"k8s.io/kubernetes/pkg/controller/daemon" "k8s.io/kubernetes/pkg/controller/daemon"
deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util" deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util"
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
@ -221,7 +220,8 @@ func (r *DaemonSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations ma
if !ok { if !ok {
return "", fmt.Errorf("passed object is not a DaemonSet: %#v", obj) return "", fmt.Errorf("passed object is not a DaemonSet: %#v", obj)
} }
allHistory, err := controlledHistories(r.c, ds) versionedClient := versionedClientsetForDaemonSet(r.c)
versionedDS, allHistory, err := controlledHistories(versionedClient, ds.Namespace, ds.Name)
if err != nil { if err != nil {
return "", fmt.Errorf("unable to find history controlled by DaemonSet %s: %v", ds.Name, err) return "", fmt.Errorf("unable to find history controlled by DaemonSet %s: %v", ds.Name, err)
} }
@ -249,55 +249,36 @@ func (r *DaemonSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations ma
return "", revisionNotFoundErr(toRevision) return "", revisionNotFoundErr(toRevision)
} }
// Get the template of the history to rollback to
toTemplate, err := getInternalTemplate(toHistory)
if err != nil {
return "", err
}
if dryRun { if dryRun {
appliedDS, err := applyHistory(versionedDS, toHistory)
if err != nil {
return "", err
}
content := bytes.NewBuffer([]byte{}) content := bytes.NewBuffer([]byte{})
w := printersinternal.NewPrefixWriter(content) w := printersinternal.NewPrefixWriter(content)
printersinternal.DescribePodTemplate(toTemplate, w) internalTemplate := &api.PodTemplateSpec{}
if err := v1.Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(&appliedDS.Spec.Template, internalTemplate, nil); err != nil {
return "", fmt.Errorf("failed to convert podtemplate while printing: %v", err)
}
printersinternal.DescribePodTemplate(internalTemplate, w)
return fmt.Sprintf("will roll back to %s", content.String()), nil return fmt.Sprintf("will roll back to %s", content.String()), nil
} }
// Update DaemonSet template, and retry on conflict // Skip if the revision already matches current DaemonSet
skipUpdate := false done, err := daemon.Match(versionedDS, toHistory)
retryErr := retry.RetryOnConflict(retry.DefaultBackoff, func() error { if err != nil {
var err error return "", err
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 { if done {
return fmt.Sprintf("%s (current template already matches revision %d)", rollbackSkipped, toRevision), nil return fmt.Sprintf("%s (current template already matches revision %d)", rollbackSkipped, toRevision), nil
} }
return rollbackSuccess, nil // Restore revision
} if _, err = versionedClient.ExtensionsV1beta1().DaemonSets(ds.Namespace).Patch(ds.Name, types.StrategicMergePatchType, toHistory.Data.Raw); err != nil {
return "", fmt.Errorf("failed restoring revision %d: %v", toRevision, err)
}
func getInternalTemplate(toHistory *appsv1beta1.ControllerRevision) (*api.PodTemplateSpec, error) { return rollbackSuccess, nil
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 { func revisionNotFoundErr(r int64) error {

View File

@ -39,6 +39,7 @@ func versionedClientsetForDaemonSet(internalClient internalclientset.Interface)
return &externalclientset.Clientset{} return &externalclientset.Clientset{}
} }
return &externalclientset.Clientset{ return &externalclientset.Clientset{
AppsV1beta1Client: apps.New(internalClient.Apps().RESTClient()), AppsV1beta1Client: apps.New(internalClient.Apps().RESTClient()),
ExtensionsV1beta1Client: extensions.New(internalClient.Extensions().RESTClient()),
} }
} }

View File

@ -268,7 +268,7 @@ var _ = framework.KubeDescribe("Daemon set [Serial]", func() {
// Check history and labels // Check history and labels
ds, err = c.Extensions().DaemonSets(ns).Get(ds.Name, metav1.GetOptions{}) ds, err = c.Extensions().DaemonSets(ns).Get(ds.Name, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
first := curHistory(listDaemonHistories(c, ns, label), &ds.Spec.Template) first := curHistory(listDaemonHistories(c, ns, label), ds)
firstHash := ds.Labels[extensions.DefaultDaemonSetUniqueLabelKey] firstHash := ds.Labels[extensions.DefaultDaemonSetUniqueLabelKey]
Expect(first.Labels[extensions.DefaultDaemonSetUniqueLabelKey]).To(Equal(firstHash)) Expect(first.Labels[extensions.DefaultDaemonSetUniqueLabelKey]).To(Equal(firstHash))
Expect(first.Revision).To(Equal(int64(1))) Expect(first.Revision).To(Equal(int64(1)))
@ -295,7 +295,7 @@ var _ = framework.KubeDescribe("Daemon set [Serial]", func() {
// Check history and labels // Check history and labels
ds, err = c.Extensions().DaemonSets(ns).Get(ds.Name, metav1.GetOptions{}) ds, err = c.Extensions().DaemonSets(ns).Get(ds.Name, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
cur := curHistory(listDaemonHistories(c, ns, label), &ds.Spec.Template) cur := curHistory(listDaemonHistories(c, ns, label), ds)
curHash := ds.Labels[extensions.DefaultDaemonSetUniqueLabelKey] curHash := ds.Labels[extensions.DefaultDaemonSetUniqueLabelKey]
Expect(cur.Labels[extensions.DefaultDaemonSetUniqueLabelKey]).To(Equal(curHash)) Expect(cur.Labels[extensions.DefaultDaemonSetUniqueLabelKey]).To(Equal(curHash))
Expect(cur.Revision).To(Equal(int64(2))) Expect(cur.Revision).To(Equal(int64(2)))
@ -325,7 +325,7 @@ var _ = framework.KubeDescribe("Daemon set [Serial]", func() {
// Check history and labels // Check history and labels
ds, err = c.Extensions().DaemonSets(ns).Get(ds.Name, metav1.GetOptions{}) ds, err = c.Extensions().DaemonSets(ns).Get(ds.Name, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
cur := curHistory(listDaemonHistories(c, ns, label), &ds.Spec.Template) cur := curHistory(listDaemonHistories(c, ns, label), ds)
hash := ds.Labels[extensions.DefaultDaemonSetUniqueLabelKey] hash := ds.Labels[extensions.DefaultDaemonSetUniqueLabelKey]
Expect(cur.Labels[extensions.DefaultDaemonSetUniqueLabelKey]).To(Equal(hash)) Expect(cur.Labels[extensions.DefaultDaemonSetUniqueLabelKey]).To(Equal(hash))
Expect(cur.Revision).To(Equal(int64(1))) Expect(cur.Revision).To(Equal(int64(1)))
@ -353,7 +353,7 @@ var _ = framework.KubeDescribe("Daemon set [Serial]", func() {
// Check history and labels // Check history and labels
ds, err = c.Extensions().DaemonSets(ns).Get(ds.Name, metav1.GetOptions{}) ds, err = c.Extensions().DaemonSets(ns).Get(ds.Name, metav1.GetOptions{})
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
cur = curHistory(listDaemonHistories(c, ns, label), &ds.Spec.Template) cur = curHistory(listDaemonHistories(c, ns, label), ds)
hash = ds.Labels[extensions.DefaultDaemonSetUniqueLabelKey] hash = ds.Labels[extensions.DefaultDaemonSetUniqueLabelKey]
Expect(cur.Labels[extensions.DefaultDaemonSetUniqueLabelKey]).To(Equal(hash)) Expect(cur.Labels[extensions.DefaultDaemonSetUniqueLabelKey]).To(Equal(hash))
Expect(cur.Revision).To(Equal(int64(2))) Expect(cur.Revision).To(Equal(int64(2)))
@ -760,14 +760,14 @@ func listDaemonHistories(c clientset.Interface, ns string, label map[string]stri
return historyList return historyList
} }
func curHistory(historyList *apps.ControllerRevisionList, template *v1.PodTemplateSpec) *apps.ControllerRevision { func curHistory(historyList *apps.ControllerRevisionList, ds *extensions.DaemonSet) *apps.ControllerRevision {
var curHistory *apps.ControllerRevision var curHistory *apps.ControllerRevision
foundCurHistories := 0 foundCurHistories := 0
for i := range historyList.Items { for i := range historyList.Items {
history := &historyList.Items[i] history := &historyList.Items[i]
// Every history should have the hash label // Every history should have the hash label
Expect(len(history.Labels[extensions.DefaultDaemonSetUniqueLabelKey])).To(BeNumerically(">", 0)) Expect(len(history.Labels[extensions.DefaultDaemonSetUniqueLabelKey])).To(BeNumerically(">", 0))
match, err := daemon.Match(template, history) match, err := daemon.Match(ds, history)
Expect(err).NotTo(HaveOccurred()) Expect(err).NotTo(HaveOccurred())
if match { if match {
curHistory = history curHistory = history