123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487 |
- /*
- Copyright 2016 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 kubectl
- import (
- "bytes"
- "fmt"
- "sort"
- appsv1 "k8s.io/api/apps/v1"
- corev1 "k8s.io/api/core/v1"
- apiequality "k8s.io/apimachinery/pkg/api/equality"
- "k8s.io/apimachinery/pkg/api/meta"
- 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/util/json"
- "k8s.io/apimachinery/pkg/util/strategicpatch"
- "k8s.io/client-go/kubernetes"
- kapps "k8s.io/kubernetes/pkg/kubectl/apps"
- "k8s.io/kubernetes/pkg/kubectl/scheme"
- deploymentutil "k8s.io/kubernetes/pkg/kubectl/util/deployment"
- )
- 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)
- }
- type RollbackVisitor struct {
- clientset kubernetes.Interface
- result Rollbacker
- }
- func (v *RollbackVisitor) VisitDeployment(elem kapps.GroupKindElement) {
- v.result = &DeploymentRollbacker{v.clientset}
- }
- func (v *RollbackVisitor) VisitStatefulSet(kind kapps.GroupKindElement) {
- v.result = &StatefulSetRollbacker{v.clientset}
- }
- func (v *RollbackVisitor) VisitDaemonSet(kind kapps.GroupKindElement) {
- v.result = &DaemonSetRollbacker{v.clientset}
- }
- func (v *RollbackVisitor) VisitJob(kind kapps.GroupKindElement) {}
- func (v *RollbackVisitor) VisitPod(kind kapps.GroupKindElement) {}
- func (v *RollbackVisitor) VisitReplicaSet(kind kapps.GroupKindElement) {}
- func (v *RollbackVisitor) VisitReplicationController(kind kapps.GroupKindElement) {}
- func (v *RollbackVisitor) VisitCronJob(kind kapps.GroupKindElement) {}
- // RollbackerFor returns an implementation of Rollbacker interface for the given schema kind
- func RollbackerFor(kind schema.GroupKind, c kubernetes.Interface) (Rollbacker, error) {
- elem := kapps.GroupKindElement(kind)
- visitor := &RollbackVisitor{
- clientset: c,
- }
- err := elem.Accept(visitor)
- if err != nil {
- return nil, fmt.Errorf("error retrieving rollbacker for %q, %v", kind.String(), err)
- }
- if visitor.result == nil {
- return nil, fmt.Errorf("no rollbacker has been implemented for %q", kind)
- }
- return visitor.result, nil
- }
- type DeploymentRollbacker struct {
- c kubernetes.Interface
- }
- func (r *DeploymentRollbacker) Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRun bool) (string, error) {
- if toRevision < 0 {
- return "", revisionNotFoundErr(toRevision)
- }
- accessor, err := meta.Accessor(obj)
- if err != nil {
- return "", fmt.Errorf("failed to create accessor for kind %v: %s", obj.GetObjectKind(), err.Error())
- }
- name := accessor.GetName()
- namespace := accessor.GetNamespace()
- // TODO: Fix this after kubectl has been removed from core. It is not possible to convert the runtime.Object
- // to the external appsv1 Deployment without round-tripping through an internal version of Deployment. We're
- // currently getting rid of all internal versions of resources. So we specifically request the appsv1 version
- // here. This follows the same pattern as for DaemonSet and StatefulSet.
- deployment, err := r.c.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{})
- if err != nil {
- return "", fmt.Errorf("failed to retrieve Deployment %s: %v", name, err)
- }
- rsForRevision, err := deploymentRevision(deployment, r.c, toRevision)
- if err != nil {
- return "", err
- }
- if dryRun {
- return printTemplate(&rsForRevision.Spec.Template)
- }
- if deployment.Spec.Paused {
- return "", fmt.Errorf("you cannot rollback a paused deployment; resume it first with 'kubectl rollout resume deployment/%s' and try again", name)
- }
- // Skip if the revision already matches current Deployment
- if equalIgnoreHash(&rsForRevision.Spec.Template, &deployment.Spec.Template) {
- return fmt.Sprintf("%s (current template already matches revision %d)", rollbackSkipped, toRevision), nil
- }
- // remove hash label before patching back into the deployment
- delete(rsForRevision.Spec.Template.Labels, appsv1.DefaultDeploymentUniqueLabelKey)
- // compute deployment annotations
- annotations := map[string]string{}
- for k := range annotationsToSkip {
- if v, ok := deployment.Annotations[k]; ok {
- annotations[k] = v
- }
- }
- for k, v := range rsForRevision.Annotations {
- if !annotationsToSkip[k] {
- annotations[k] = v
- }
- }
- // make patch to restore
- patchType, patch, err := getDeploymentPatch(&rsForRevision.Spec.Template, annotations)
- if err != nil {
- return "", fmt.Errorf("failed restoring revision %d: %v", toRevision, err)
- }
- // Restore revision
- if _, err = r.c.AppsV1().Deployments(namespace).Patch(name, patchType, patch); err != nil {
- return "", fmt.Errorf("failed restoring revision %d: %v", toRevision, err)
- }
- return rollbackSuccess, nil
- }
- // equalIgnoreHash returns true if two given podTemplateSpec are equal, ignoring the diff in value of Labels[pod-template-hash]
- // We ignore pod-template-hash because:
- // 1. The hash result would be different upon podTemplateSpec API changes
- // (e.g. the addition of a new field will cause the hash code to change)
- // 2. The deployment template won't have hash labels
- func equalIgnoreHash(template1, template2 *corev1.PodTemplateSpec) bool {
- t1Copy := template1.DeepCopy()
- t2Copy := template2.DeepCopy()
- // Remove hash labels from template.Labels before comparing
- delete(t1Copy.Labels, appsv1.DefaultDeploymentUniqueLabelKey)
- delete(t2Copy.Labels, appsv1.DefaultDeploymentUniqueLabelKey)
- return apiequality.Semantic.DeepEqual(t1Copy, t2Copy)
- }
- // annotationsToSkip lists the annotations that should be preserved from the deployment and not
- // copied from the replicaset when rolling a deployment back
- var annotationsToSkip = map[string]bool{
- corev1.LastAppliedConfigAnnotation: true,
- deploymentutil.RevisionAnnotation: true,
- deploymentutil.RevisionHistoryAnnotation: true,
- deploymentutil.DesiredReplicasAnnotation: true,
- deploymentutil.MaxReplicasAnnotation: true,
- appsv1.DeprecatedRollbackTo: true,
- }
- // getPatch returns a patch that can be applied to restore a Deployment to a
- // previous version. If the returned error is nil the patch is valid.
- func getDeploymentPatch(podTemplate *corev1.PodTemplateSpec, annotations map[string]string) (types.PatchType, []byte, error) {
- // Create a patch of the Deployment that replaces spec.template
- patch, err := json.Marshal([]interface{}{
- map[string]interface{}{
- "op": "replace",
- "path": "/spec/template",
- "value": podTemplate,
- },
- map[string]interface{}{
- "op": "replace",
- "path": "/metadata/annotations",
- "value": annotations,
- },
- })
- return types.JSONPatchType, patch, err
- }
- func deploymentRevision(deployment *appsv1.Deployment, c kubernetes.Interface, toRevision int64) (revision *appsv1.ReplicaSet, err error) {
- _, allOldRSs, newRS, err := deploymentutil.GetAllReplicaSets(deployment, c.AppsV1())
- if err != nil {
- return nil, fmt.Errorf("failed to retrieve replica sets from deployment %s: %v", deployment.Name, err)
- }
- allRSs := allOldRSs
- if newRS != nil {
- allRSs = append(allRSs, newRS)
- }
- var (
- latestReplicaSet *appsv1.ReplicaSet
- latestRevision = int64(-1)
- previousReplicaSet *appsv1.ReplicaSet
- previousRevision = int64(-1)
- )
- for _, rs := range allRSs {
- if v, err := deploymentutil.Revision(rs); err == nil {
- if toRevision == 0 {
- if latestRevision < v {
- // newest one we've seen so far
- previousRevision = latestRevision
- previousReplicaSet = latestReplicaSet
- latestRevision = v
- latestReplicaSet = rs
- } else if previousRevision < v {
- // second newest one we've seen so far
- previousRevision = v
- previousReplicaSet = rs
- }
- } else if toRevision == v {
- return rs, nil
- }
- }
- }
- if toRevision > 0 {
- return nil, revisionNotFoundErr(toRevision)
- }
- if previousReplicaSet == nil {
- return nil, fmt.Errorf("no rollout history found for deployment %q", deployment.Name)
- }
- return previousReplicaSet, nil
- }
- type DaemonSetRollbacker struct {
- c kubernetes.Interface
- }
- func (r *DaemonSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRun bool) (string, error) {
- if toRevision < 0 {
- return "", revisionNotFoundErr(toRevision)
- }
- accessor, err := meta.Accessor(obj)
- if err != nil {
- return "", fmt.Errorf("failed to create accessor for kind %v: %s", obj.GetObjectKind(), err.Error())
- }
- ds, history, err := daemonSetHistory(r.c.AppsV1(), accessor.GetNamespace(), accessor.GetName())
- if err != nil {
- return "", err
- }
- if toRevision == 0 && len(history) <= 1 {
- return "", fmt.Errorf("no last revision to roll back to")
- }
- toHistory := findHistory(toRevision, history)
- if toHistory == nil {
- return "", revisionNotFoundErr(toRevision)
- }
- if dryRun {
- appliedDS, err := applyDaemonSetHistory(ds, toHistory)
- if err != nil {
- return "", err
- }
- return printPodTemplate(&appliedDS.Spec.Template)
- }
- // Skip if the revision already matches current DaemonSet
- done, err := daemonSetMatch(ds, toHistory)
- if err != nil {
- return "", err
- }
- if done {
- return fmt.Sprintf("%s (current template already matches revision %d)", rollbackSkipped, toRevision), nil
- }
- // Restore revision
- if _, err = r.c.AppsV1().DaemonSets(accessor.GetNamespace()).Patch(accessor.GetName(), types.StrategicMergePatchType, toHistory.Data.Raw); err != nil {
- return "", fmt.Errorf("failed restoring revision %d: %v", toRevision, err)
- }
- 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
- }
- // toRevision is a non-negative integer, with 0 being reserved to indicate rolling back to previous configuration
- func (r *StatefulSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRun bool) (string, error) {
- if toRevision < 0 {
- return "", revisionNotFoundErr(toRevision)
- }
- accessor, err := meta.Accessor(obj)
- if err != nil {
- return "", fmt.Errorf("failed to create accessor for kind %v: %s", obj.GetObjectKind(), err.Error())
- }
- sts, history, err := statefulSetHistory(r.c.AppsV1(), accessor.GetNamespace(), accessor.GetName())
- if err != nil {
- return "", err
- }
- if toRevision == 0 && len(history) <= 1 {
- return "", fmt.Errorf("no last revision to roll back to")
- }
- toHistory := findHistory(toRevision, history)
- if toHistory == nil {
- return "", revisionNotFoundErr(toRevision)
- }
- if dryRun {
- appliedSS, err := applyRevision(sts, toHistory)
- if err != nil {
- return "", err
- }
- return printPodTemplate(&appliedSS.Spec.Template)
- }
- // Skip if the revision already matches current StatefulSet
- done, err := statefulsetMatch(sts, toHistory)
- if err != nil {
- return "", err
- }
- if done {
- return fmt.Sprintf("%s (current template already matches revision %d)", rollbackSkipped, toRevision), nil
- }
- // Restore revision
- if _, err = r.c.AppsV1().StatefulSets(sts.Namespace).Patch(sts.Name, types.StrategicMergePatchType, toHistory.Data.Raw); err != nil {
- return "", fmt.Errorf("failed restoring revision %d: %v", toRevision, err)
- }
- return rollbackSuccess, nil
- }
- var appsCodec = scheme.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{}
- if err := json.Unmarshal([]byte(str), &raw); err != nil {
- return nil, err
- }
- 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.
- func findHistory(toRevision int64, allHistory []*appsv1.ControllerRevision) *appsv1.ControllerRevision {
- if toRevision == 0 && len(allHistory) <= 1 {
- return nil
- }
- // Find the history to rollback to
- var toHistory *appsv1.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
- return h
- }
- }
- }
- return toHistory
- }
- // printPodTemplate converts a given pod template into a human-readable string.
- func printPodTemplate(specTemplate *corev1.PodTemplateSpec) (string, error) {
- podSpec, err := printTemplate(specTemplate)
- if err != nil {
- return "", err
- }
- return fmt.Sprintf("will roll back to %s", podSpec), 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 []*appsv1.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
- }
|