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
```
This commit is contained in:
Kubernetes Submit Queue
2017-06-12 23:31:08 -07:00
committed by GitHub
9 changed files with 131 additions and 90 deletions

View File

@@ -154,7 +154,6 @@ 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",
@@ -167,7 +166,9 @@ go_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/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/strategicpatch: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/wait:go_default_library",

View File

@@ -26,14 +26,17 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"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/v1"
"k8s.io/kubernetes/pkg/apis/apps"
appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1"
"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"
"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"
@@ -149,13 +152,10 @@ type DaemonSetHistoryViewer struct {
// 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{})
versionedClient := versionedClientsetForDaemonSet(h.c)
ds, allHistory, err := controlledHistories(versionedClient, namespace, name)
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)
return "", fmt.Errorf("unable to find history controlled by DaemonSet %s: %v", name, err)
}
historyInfo := make(map[int64]*appsv1beta1.ControllerRevision)
for _, history := range allHistory {
@@ -173,11 +173,11 @@ func (h *DaemonSetHistoryViewer) ViewHistory(namespace, name string, revision in
if !ok {
return "", fmt.Errorf("unable to find the specified revision")
}
template, err := daemon.DecodeHistory(history)
dsOfHistory, err := applyHistory(ds, history)
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
@@ -255,17 +255,19 @@ func (h *StatefulSetHistoryViewer) ViewHistory(namespace, name string, revision
}
// 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) {
func controlledHistories(c externalclientset.Interface, namespace, name string) (*extensionsv1beta1.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
selector, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector)
if err != nil {
return nil, err
return nil, nil, err
}
versionedClient := versionedClientsetForDaemonSet(c)
historyList, err := versionedClient.AppsV1beta1().ControllerRevisions(ds.Namespace).List(metav1.ListOptions{LabelSelector: selector.String()})
historyList, err := c.AppsV1beta1().ControllerRevisions(ds.Namespace).List(metav1.ListOptions{LabelSelector: selector.String()})
if err != nil {
return nil, err
return nil, nil, err
}
for i := range historyList.Items {
history := historyList.Items[i]
@@ -275,7 +277,29 @@ func controlledHistories(c clientset.Interface, ds *extensions.DaemonSet) ([]*ap
}
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

View File

@@ -24,10 +24,10 @@ import (
"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"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/v1"
@@ -36,7 +36,6 @@ import (
"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"
@@ -221,7 +220,8 @@ func (r *DaemonSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations ma
if !ok {
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 {
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)
}
// Get the template of the history to rollback to
toTemplate, err := getInternalTemplate(toHistory)
if err != nil {
return "", err
}
if dryRun {
appliedDS, err := applyHistory(versionedDS, toHistory)
if err != nil {
return "", err
}
content := bytes.NewBuffer([]byte{})
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
}
// 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
// Skip if the revision already matches current DaemonSet
done, err := daemon.Match(versionedDS, toHistory)
if err != nil {
return "", err
}
if skipUpdate {
if done {
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) {
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
return rollbackSuccess, nil
}
func revisionNotFoundErr(r int64) error {

View File

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