...

Source file src/k8s.io/kubectl/pkg/polymorphichelpers/history.go

Documentation: k8s.io/kubectl/pkg/polymorphichelpers

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package polymorphichelpers
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"fmt"
    23  	"io"
    24  	"text/tabwriter"
    25  
    26  	appsv1 "k8s.io/api/apps/v1"
    27  	corev1 "k8s.io/api/core/v1"
    28  	"k8s.io/apimachinery/pkg/api/meta"
    29  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    30  	"k8s.io/apimachinery/pkg/labels"
    31  	"k8s.io/apimachinery/pkg/runtime"
    32  	"k8s.io/apimachinery/pkg/runtime/schema"
    33  	"k8s.io/apimachinery/pkg/util/json"
    34  	"k8s.io/apimachinery/pkg/util/strategicpatch"
    35  	"k8s.io/client-go/kubernetes"
    36  	clientappsv1 "k8s.io/client-go/kubernetes/typed/apps/v1"
    37  	"k8s.io/klog/v2"
    38  	"k8s.io/kubectl/pkg/apps"
    39  	"k8s.io/kubectl/pkg/describe"
    40  	deploymentutil "k8s.io/kubectl/pkg/util/deployment"
    41  	sliceutil "k8s.io/kubectl/pkg/util/slice"
    42  )
    43  
    44  const (
    45  	ChangeCauseAnnotation = "kubernetes.io/change-cause"
    46  )
    47  
    48  // HistoryViewer provides an interface for resources have historical information.
    49  type HistoryViewer interface {
    50  	ViewHistory(namespace, name string, revision int64) (string, error)
    51  	GetHistory(namespace, name string) (map[int64]runtime.Object, error)
    52  }
    53  
    54  type HistoryVisitor struct {
    55  	clientset kubernetes.Interface
    56  	result    HistoryViewer
    57  }
    58  
    59  func (v *HistoryVisitor) VisitDeployment(elem apps.GroupKindElement) {
    60  	v.result = &DeploymentHistoryViewer{v.clientset}
    61  }
    62  
    63  func (v *HistoryVisitor) VisitStatefulSet(kind apps.GroupKindElement) {
    64  	v.result = &StatefulSetHistoryViewer{v.clientset}
    65  }
    66  
    67  func (v *HistoryVisitor) VisitDaemonSet(kind apps.GroupKindElement) {
    68  	v.result = &DaemonSetHistoryViewer{v.clientset}
    69  }
    70  
    71  func (v *HistoryVisitor) VisitJob(kind apps.GroupKindElement)                   {}
    72  func (v *HistoryVisitor) VisitPod(kind apps.GroupKindElement)                   {}
    73  func (v *HistoryVisitor) VisitReplicaSet(kind apps.GroupKindElement)            {}
    74  func (v *HistoryVisitor) VisitReplicationController(kind apps.GroupKindElement) {}
    75  func (v *HistoryVisitor) VisitCronJob(kind apps.GroupKindElement)               {}
    76  
    77  // HistoryViewerFor returns an implementation of HistoryViewer interface for the given schema kind
    78  func HistoryViewerFor(kind schema.GroupKind, c kubernetes.Interface) (HistoryViewer, error) {
    79  	elem := apps.GroupKindElement(kind)
    80  	visitor := &HistoryVisitor{
    81  		clientset: c,
    82  	}
    83  
    84  	// Determine which HistoryViewer we need here
    85  	err := elem.Accept(visitor)
    86  
    87  	if err != nil {
    88  		return nil, fmt.Errorf("error retrieving history for %q, %v", kind.String(), err)
    89  	}
    90  
    91  	if visitor.result == nil {
    92  		return nil, fmt.Errorf("no history viewer has been implemented for %q", kind.String())
    93  	}
    94  
    95  	return visitor.result, nil
    96  }
    97  
    98  type DeploymentHistoryViewer struct {
    99  	c kubernetes.Interface
   100  }
   101  
   102  // ViewHistory returns a revision-to-replicaset map as the revision history of a deployment
   103  // TODO: this should be a describer
   104  func (h *DeploymentHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
   105  	allRSs, err := getDeploymentReplicaSets(h.c.AppsV1(), namespace, name)
   106  	if err != nil {
   107  		return "", err
   108  	}
   109  
   110  	historyInfo := make(map[int64]*corev1.PodTemplateSpec)
   111  	for _, rs := range allRSs {
   112  		v, err := deploymentutil.Revision(rs)
   113  		if err != nil {
   114  			klog.Warningf("unable to get revision from replicaset %s for deployment %s in namespace %s: %v", rs.Name, name, namespace, err)
   115  			continue
   116  		}
   117  		historyInfo[v] = &rs.Spec.Template
   118  		changeCause := getChangeCause(rs)
   119  		if historyInfo[v].Annotations == nil {
   120  			historyInfo[v].Annotations = make(map[string]string)
   121  		}
   122  		if len(changeCause) > 0 {
   123  			historyInfo[v].Annotations[ChangeCauseAnnotation] = changeCause
   124  		}
   125  	}
   126  
   127  	if len(historyInfo) == 0 {
   128  		return "No rollout history found.", nil
   129  	}
   130  
   131  	if revision > 0 {
   132  		// Print details of a specific revision
   133  		template, ok := historyInfo[revision]
   134  		if !ok {
   135  			return "", fmt.Errorf("unable to find the specified revision")
   136  		}
   137  		return printTemplate(template)
   138  	}
   139  
   140  	// Sort the revisionToChangeCause map by revision
   141  	revisions := make([]int64, 0, len(historyInfo))
   142  	for r := range historyInfo {
   143  		revisions = append(revisions, r)
   144  	}
   145  	sliceutil.SortInts64(revisions)
   146  
   147  	return tabbedString(func(out io.Writer) error {
   148  		fmt.Fprintf(out, "REVISION\tCHANGE-CAUSE\n")
   149  		for _, r := range revisions {
   150  			// Find the change-cause of revision r
   151  			changeCause := historyInfo[r].Annotations[ChangeCauseAnnotation]
   152  			if len(changeCause) == 0 {
   153  				changeCause = "<none>"
   154  			}
   155  			fmt.Fprintf(out, "%d\t%s\n", r, changeCause)
   156  		}
   157  		return nil
   158  	})
   159  }
   160  
   161  // GetHistory returns the ReplicaSet revisions associated with a Deployment
   162  func (h *DeploymentHistoryViewer) GetHistory(namespace, name string) (map[int64]runtime.Object, error) {
   163  	allRSs, err := getDeploymentReplicaSets(h.c.AppsV1(), namespace, name)
   164  	if err != nil {
   165  		return nil, err
   166  	}
   167  
   168  	result := make(map[int64]runtime.Object)
   169  	for _, rs := range allRSs {
   170  		v, err := deploymentutil.Revision(rs)
   171  		if err != nil {
   172  			klog.Warningf("unable to get revision from replicaset %s for deployment %s in namespace %s: %v", rs.Name, name, namespace, err)
   173  			continue
   174  		}
   175  		result[v] = rs
   176  	}
   177  
   178  	return result, nil
   179  }
   180  
   181  func printTemplate(template *corev1.PodTemplateSpec) (string, error) {
   182  	buf := bytes.NewBuffer([]byte{})
   183  	w := describe.NewPrefixWriter(buf)
   184  	describe.DescribePodTemplate(template, w)
   185  	return buf.String(), nil
   186  }
   187  
   188  type DaemonSetHistoryViewer struct {
   189  	c kubernetes.Interface
   190  }
   191  
   192  // ViewHistory returns a revision-to-history map as the revision history of a deployment
   193  // TODO: this should be a describer
   194  func (h *DaemonSetHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
   195  	ds, history, err := daemonSetHistory(h.c.AppsV1(), namespace, name)
   196  	if err != nil {
   197  		return "", err
   198  	}
   199  	return printHistory(history, revision, func(history *appsv1.ControllerRevision) (*corev1.PodTemplateSpec, error) {
   200  		dsOfHistory, err := applyDaemonSetHistory(ds, history)
   201  		if err != nil {
   202  			return nil, err
   203  		}
   204  		return &dsOfHistory.Spec.Template, err
   205  	})
   206  }
   207  
   208  // GetHistory returns the revisions associated with a DaemonSet
   209  func (h *DaemonSetHistoryViewer) GetHistory(namespace, name string) (map[int64]runtime.Object, error) {
   210  	ds, history, err := daemonSetHistory(h.c.AppsV1(), namespace, name)
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  
   215  	result := make(map[int64]runtime.Object)
   216  	for _, h := range history {
   217  		applied, err := applyDaemonSetHistory(ds, h)
   218  		if err != nil {
   219  			return nil, err
   220  		}
   221  		result[h.Revision] = applied
   222  	}
   223  
   224  	return result, nil
   225  }
   226  
   227  // printHistory returns the podTemplate of the given revision if it is non-zero
   228  // else returns the overall revisions
   229  func printHistory(history []*appsv1.ControllerRevision, revision int64, getPodTemplate func(history *appsv1.ControllerRevision) (*corev1.PodTemplateSpec, error)) (string, error) {
   230  	historyInfo := make(map[int64]*appsv1.ControllerRevision)
   231  	for _, history := range history {
   232  		// TODO: for now we assume revisions don't overlap, we may need to handle it
   233  		historyInfo[history.Revision] = history
   234  	}
   235  	if len(historyInfo) == 0 {
   236  		return "No rollout history found.", nil
   237  	}
   238  
   239  	// Print details of a specific revision
   240  	if revision > 0 {
   241  		history, ok := historyInfo[revision]
   242  		if !ok {
   243  			return "", fmt.Errorf("unable to find the specified revision")
   244  		}
   245  		podTemplate, err := getPodTemplate(history)
   246  		if err != nil {
   247  			return "", fmt.Errorf("unable to parse history %s", history.Name)
   248  		}
   249  		return printTemplate(podTemplate)
   250  	}
   251  
   252  	// Print an overview of all Revisions
   253  	// Sort the revisionToChangeCause map by revision
   254  	revisions := make([]int64, 0, len(historyInfo))
   255  	for r := range historyInfo {
   256  		revisions = append(revisions, r)
   257  	}
   258  	sliceutil.SortInts64(revisions)
   259  
   260  	return tabbedString(func(out io.Writer) error {
   261  		fmt.Fprintf(out, "REVISION\tCHANGE-CAUSE\n")
   262  		for _, r := range revisions {
   263  			// Find the change-cause of revision r
   264  			changeCause := historyInfo[r].Annotations[ChangeCauseAnnotation]
   265  			if len(changeCause) == 0 {
   266  				changeCause = "<none>"
   267  			}
   268  			fmt.Fprintf(out, "%d\t%s\n", r, changeCause)
   269  		}
   270  		return nil
   271  	})
   272  }
   273  
   274  type StatefulSetHistoryViewer struct {
   275  	c kubernetes.Interface
   276  }
   277  
   278  // ViewHistory returns a list of the revision history of a statefulset
   279  // TODO: this should be a describer
   280  func (h *StatefulSetHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
   281  	sts, history, err := statefulSetHistory(h.c.AppsV1(), namespace, name)
   282  	if err != nil {
   283  		return "", err
   284  	}
   285  	return printHistory(history, revision, func(history *appsv1.ControllerRevision) (*corev1.PodTemplateSpec, error) {
   286  		stsOfHistory, err := applyStatefulSetHistory(sts, history)
   287  		if err != nil {
   288  			return nil, err
   289  		}
   290  		return &stsOfHistory.Spec.Template, err
   291  	})
   292  }
   293  
   294  // GetHistory returns the revisions associated with a StatefulSet
   295  func (h *StatefulSetHistoryViewer) GetHistory(namespace, name string) (map[int64]runtime.Object, error) {
   296  	sts, history, err := statefulSetHistory(h.c.AppsV1(), namespace, name)
   297  	if err != nil {
   298  		return nil, err
   299  	}
   300  
   301  	result := make(map[int64]runtime.Object)
   302  	for _, h := range history {
   303  		applied, err := applyStatefulSetHistory(sts, h)
   304  		if err != nil {
   305  			return nil, err
   306  		}
   307  		result[h.Revision] = applied
   308  	}
   309  
   310  	return result, nil
   311  }
   312  
   313  func getDeploymentReplicaSets(apps clientappsv1.AppsV1Interface, namespace, name string) ([]*appsv1.ReplicaSet, error) {
   314  	deployment, err := apps.Deployments(namespace).Get(context.TODO(), name, metav1.GetOptions{})
   315  	if err != nil {
   316  		return nil, fmt.Errorf("failed to retrieve deployment %s: %v", name, err)
   317  	}
   318  
   319  	_, oldRSs, newRS, err := deploymentutil.GetAllReplicaSets(deployment, apps)
   320  	if err != nil {
   321  		return nil, fmt.Errorf("failed to retrieve replica sets from deployment %s: %v", name, err)
   322  	}
   323  
   324  	if newRS == nil {
   325  		return oldRSs, nil
   326  	}
   327  	return append(oldRSs, newRS), nil
   328  }
   329  
   330  // controlledHistories returns all ControllerRevisions in namespace that selected by selector and owned by accessor
   331  // TODO: Rename this to controllerHistory when other controllers have been upgraded
   332  func controlledHistoryV1(
   333  	apps clientappsv1.AppsV1Interface,
   334  	namespace string,
   335  	selector labels.Selector,
   336  	accessor metav1.Object) ([]*appsv1.ControllerRevision, error) {
   337  	var result []*appsv1.ControllerRevision
   338  	historyList, err := apps.ControllerRevisions(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()})
   339  	if err != nil {
   340  		return nil, err
   341  	}
   342  	for i := range historyList.Items {
   343  		history := historyList.Items[i]
   344  		// Only add history that belongs to the API object
   345  		if metav1.IsControlledBy(&history, accessor) {
   346  			result = append(result, &history)
   347  		}
   348  	}
   349  	return result, nil
   350  }
   351  
   352  // controlledHistories returns all ControllerRevisions in namespace that selected by selector and owned by accessor
   353  func controlledHistory(
   354  	apps clientappsv1.AppsV1Interface,
   355  	namespace string,
   356  	selector labels.Selector,
   357  	accessor metav1.Object) ([]*appsv1.ControllerRevision, error) {
   358  	var result []*appsv1.ControllerRevision
   359  	historyList, err := apps.ControllerRevisions(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: selector.String()})
   360  	if err != nil {
   361  		return nil, err
   362  	}
   363  	for i := range historyList.Items {
   364  		history := historyList.Items[i]
   365  		// Only add history that belongs to the API object
   366  		if metav1.IsControlledBy(&history, accessor) {
   367  			result = append(result, &history)
   368  		}
   369  	}
   370  	return result, nil
   371  }
   372  
   373  // daemonSetHistory returns the DaemonSet named name in namespace and all ControllerRevisions in its history.
   374  func daemonSetHistory(
   375  	apps clientappsv1.AppsV1Interface,
   376  	namespace, name string) (*appsv1.DaemonSet, []*appsv1.ControllerRevision, error) {
   377  	ds, err := apps.DaemonSets(namespace).Get(context.TODO(), name, metav1.GetOptions{})
   378  	if err != nil {
   379  		return nil, nil, fmt.Errorf("failed to retrieve DaemonSet %s: %v", name, err)
   380  	}
   381  	selector, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector)
   382  	if err != nil {
   383  		return nil, nil, fmt.Errorf("failed to create selector for DaemonSet %s: %v", ds.Name, err)
   384  	}
   385  	accessor, err := meta.Accessor(ds)
   386  	if err != nil {
   387  		return nil, nil, fmt.Errorf("failed to create accessor for DaemonSet %s: %v", ds.Name, err)
   388  	}
   389  	history, err := controlledHistory(apps, ds.Namespace, selector, accessor)
   390  	if err != nil {
   391  		return nil, nil, fmt.Errorf("unable to find history controlled by DaemonSet %s: %v", ds.Name, err)
   392  	}
   393  	return ds, history, nil
   394  }
   395  
   396  // statefulSetHistory returns the StatefulSet named name in namespace and all ControllerRevisions in its history.
   397  func statefulSetHistory(
   398  	apps clientappsv1.AppsV1Interface,
   399  	namespace, name string) (*appsv1.StatefulSet, []*appsv1.ControllerRevision, error) {
   400  	sts, err := apps.StatefulSets(namespace).Get(context.TODO(), name, metav1.GetOptions{})
   401  	if err != nil {
   402  		return nil, nil, fmt.Errorf("failed to retrieve Statefulset %s: %s", name, err.Error())
   403  	}
   404  	selector, err := metav1.LabelSelectorAsSelector(sts.Spec.Selector)
   405  	if err != nil {
   406  		return nil, nil, fmt.Errorf("failed to create selector for StatefulSet %s: %s", name, err.Error())
   407  	}
   408  	accessor, err := meta.Accessor(sts)
   409  	if err != nil {
   410  		return nil, nil, fmt.Errorf("failed to obtain accessor for StatefulSet %s: %s", name, err.Error())
   411  	}
   412  	history, err := controlledHistoryV1(apps, namespace, selector, accessor)
   413  	if err != nil {
   414  		return nil, nil, fmt.Errorf("unable to find history controlled by StatefulSet %s: %v", name, err)
   415  	}
   416  	return sts, history, nil
   417  }
   418  
   419  // applyDaemonSetHistory returns a specific revision of DaemonSet by applying the given history to a copy of the given DaemonSet
   420  func applyDaemonSetHistory(ds *appsv1.DaemonSet, history *appsv1.ControllerRevision) (*appsv1.DaemonSet, error) {
   421  	dsBytes, err := json.Marshal(ds)
   422  	if err != nil {
   423  		return nil, err
   424  	}
   425  	patched, err := strategicpatch.StrategicMergePatch(dsBytes, history.Data.Raw, ds)
   426  	if err != nil {
   427  		return nil, err
   428  	}
   429  	result := &appsv1.DaemonSet{}
   430  	err = json.Unmarshal(patched, result)
   431  	if err != nil {
   432  		return nil, err
   433  	}
   434  	return result, nil
   435  }
   436  
   437  // applyStatefulSetHistory returns a specific revision of StatefulSet by applying the given history to a copy of the given StatefulSet
   438  func applyStatefulSetHistory(sts *appsv1.StatefulSet, history *appsv1.ControllerRevision) (*appsv1.StatefulSet, error) {
   439  	stsBytes, err := json.Marshal(sts)
   440  	if err != nil {
   441  		return nil, err
   442  	}
   443  	patched, err := strategicpatch.StrategicMergePatch(stsBytes, history.Data.Raw, sts)
   444  	if err != nil {
   445  		return nil, err
   446  	}
   447  	result := &appsv1.StatefulSet{}
   448  	err = json.Unmarshal(patched, result)
   449  	if err != nil {
   450  		return nil, err
   451  	}
   452  	return result, nil
   453  }
   454  
   455  // TODO: copied here until this becomes a describer
   456  func tabbedString(f func(io.Writer) error) (string, error) {
   457  	out := new(tabwriter.Writer)
   458  	buf := &bytes.Buffer{}
   459  	out.Init(buf, 0, 8, 2, ' ', 0)
   460  
   461  	err := f(out)
   462  	if err != nil {
   463  		return "", err
   464  	}
   465  
   466  	out.Flush()
   467  	return buf.String(), nil
   468  }
   469  
   470  // getChangeCause returns the change-cause annotation of the input object
   471  func getChangeCause(obj runtime.Object) string {
   472  	accessor, err := meta.Accessor(obj)
   473  	if err != nil {
   474  		return ""
   475  	}
   476  	return accessor.GetAnnotations()[ChangeCauseAnnotation]
   477  }
   478  

View as plain text