mirror of
				https://github.com/k3s-io/kubernetes.git
				synced 2025-10-31 05:40:42 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			391 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			391 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| 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"
 | |
| 	"io"
 | |
| 	"text/tabwriter"
 | |
| 
 | |
| 	appsv1 "k8s.io/api/apps/v1"
 | |
| 	corev1 "k8s.io/api/core/v1"
 | |
| 
 | |
| 	"k8s.io/apimachinery/pkg/api/meta"
 | |
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | |
| 	"k8s.io/apimachinery/pkg/labels"
 | |
| 	"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/client-go/kubernetes"
 | |
| 	clientappsv1 "k8s.io/client-go/kubernetes/typed/apps/v1"
 | |
| 	kapps "k8s.io/kubernetes/pkg/kubectl/apps"
 | |
| 	describe "k8s.io/kubernetes/pkg/kubectl/describe/versioned"
 | |
| 	deploymentutil "k8s.io/kubernetes/pkg/kubectl/util/deployment"
 | |
| 	sliceutil "k8s.io/kubernetes/pkg/kubectl/util/slice"
 | |
| )
 | |
| 
 | |
| const (
 | |
| 	ChangeCauseAnnotation = "kubernetes.io/change-cause"
 | |
| )
 | |
| 
 | |
| // HistoryViewer provides an interface for resources have historical information.
 | |
| type HistoryViewer interface {
 | |
| 	ViewHistory(namespace, name string, revision int64) (string, error)
 | |
| }
 | |
| 
 | |
| type HistoryVisitor struct {
 | |
| 	clientset kubernetes.Interface
 | |
| 	result    HistoryViewer
 | |
| }
 | |
| 
 | |
| func (v *HistoryVisitor) VisitDeployment(elem kapps.GroupKindElement) {
 | |
| 	v.result = &DeploymentHistoryViewer{v.clientset}
 | |
| }
 | |
| 
 | |
| func (v *HistoryVisitor) VisitStatefulSet(kind kapps.GroupKindElement) {
 | |
| 	v.result = &StatefulSetHistoryViewer{v.clientset}
 | |
| }
 | |
| 
 | |
| func (v *HistoryVisitor) VisitDaemonSet(kind kapps.GroupKindElement) {
 | |
| 	v.result = &DaemonSetHistoryViewer{v.clientset}
 | |
| }
 | |
| 
 | |
| func (v *HistoryVisitor) VisitJob(kind kapps.GroupKindElement)                   {}
 | |
| func (v *HistoryVisitor) VisitPod(kind kapps.GroupKindElement)                   {}
 | |
| func (v *HistoryVisitor) VisitReplicaSet(kind kapps.GroupKindElement)            {}
 | |
| func (v *HistoryVisitor) VisitReplicationController(kind kapps.GroupKindElement) {}
 | |
| func (v *HistoryVisitor) VisitCronJob(kind kapps.GroupKindElement)               {}
 | |
| 
 | |
| // HistoryViewerFor returns an implementation of HistoryViewer interface for the given schema kind
 | |
| func HistoryViewerFor(kind schema.GroupKind, c kubernetes.Interface) (HistoryViewer, error) {
 | |
| 	elem := kapps.GroupKindElement(kind)
 | |
| 	visitor := &HistoryVisitor{
 | |
| 		clientset: c,
 | |
| 	}
 | |
| 
 | |
| 	// Determine which HistoryViewer we need here
 | |
| 	err := elem.Accept(visitor)
 | |
| 
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("error retrieving history for %q, %v", kind.String(), err)
 | |
| 	}
 | |
| 
 | |
| 	if visitor.result == nil {
 | |
| 		return nil, fmt.Errorf("no history viewer has been implemented for %q", kind.String())
 | |
| 	}
 | |
| 
 | |
| 	return visitor.result, nil
 | |
| }
 | |
| 
 | |
| type DeploymentHistoryViewer struct {
 | |
| 	c kubernetes.Interface
 | |
| }
 | |
| 
 | |
| // ViewHistory returns a revision-to-replicaset map as the revision history of a deployment
 | |
| // TODO: this should be a describer
 | |
| func (h *DeploymentHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
 | |
| 	versionedAppsClient := h.c.AppsV1()
 | |
| 	deployment, err := versionedAppsClient.Deployments(namespace).Get(name, metav1.GetOptions{})
 | |
| 	if err != nil {
 | |
| 		return "", fmt.Errorf("failed to retrieve deployment %s: %v", name, err)
 | |
| 	}
 | |
| 	_, allOldRSs, newRS, err := deploymentutil.GetAllReplicaSets(deployment, versionedAppsClient)
 | |
| 	if err != nil {
 | |
| 		return "", fmt.Errorf("failed to retrieve replica sets from deployment %s: %v", name, err)
 | |
| 	}
 | |
| 	allRSs := allOldRSs
 | |
| 	if newRS != nil {
 | |
| 		allRSs = append(allRSs, newRS)
 | |
| 	}
 | |
| 
 | |
| 	historyInfo := make(map[int64]*corev1.PodTemplateSpec)
 | |
| 	for _, rs := range allRSs {
 | |
| 		v, err := deploymentutil.Revision(rs)
 | |
| 		if err != nil {
 | |
| 			continue
 | |
| 		}
 | |
| 		historyInfo[v] = &rs.Spec.Template
 | |
| 		changeCause := getChangeCause(rs)
 | |
| 		if historyInfo[v].Annotations == nil {
 | |
| 			historyInfo[v].Annotations = make(map[string]string)
 | |
| 		}
 | |
| 		if len(changeCause) > 0 {
 | |
| 			historyInfo[v].Annotations[ChangeCauseAnnotation] = changeCause
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if len(historyInfo) == 0 {
 | |
| 		return "No rollout history found.", nil
 | |
| 	}
 | |
| 
 | |
| 	if revision > 0 {
 | |
| 		// Print details of a specific revision
 | |
| 		template, ok := historyInfo[revision]
 | |
| 		if !ok {
 | |
| 			return "", fmt.Errorf("unable to find the specified revision")
 | |
| 		}
 | |
| 		return printTemplate(template)
 | |
| 	}
 | |
| 
 | |
| 	// Sort the revisionToChangeCause map by revision
 | |
| 	revisions := make([]int64, 0, len(historyInfo))
 | |
| 	for r := range historyInfo {
 | |
| 		revisions = append(revisions, r)
 | |
| 	}
 | |
| 	sliceutil.SortInts64(revisions)
 | |
| 
 | |
| 	return tabbedString(func(out io.Writer) error {
 | |
| 		fmt.Fprintf(out, "REVISION\tCHANGE-CAUSE\n")
 | |
| 		for _, r := range revisions {
 | |
| 			// Find the change-cause of revision r
 | |
| 			changeCause := historyInfo[r].Annotations[ChangeCauseAnnotation]
 | |
| 			if len(changeCause) == 0 {
 | |
| 				changeCause = "<none>"
 | |
| 			}
 | |
| 			fmt.Fprintf(out, "%d\t%s\n", r, changeCause)
 | |
| 		}
 | |
| 		return nil
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func printTemplate(template *corev1.PodTemplateSpec) (string, error) {
 | |
| 	buf := bytes.NewBuffer([]byte{})
 | |
| 	w := describe.NewPrefixWriter(buf)
 | |
| 	describe.DescribePodTemplate(template, w)
 | |
| 	return buf.String(), nil
 | |
| }
 | |
| 
 | |
| type DaemonSetHistoryViewer struct {
 | |
| 	c kubernetes.Interface
 | |
| }
 | |
| 
 | |
| // 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, history, err := daemonSetHistory(h.c.AppsV1(), namespace, name)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	historyInfo := make(map[int64]*appsv1.ControllerRevision)
 | |
| 	for _, history := range history {
 | |
| 		// TODO: for now we assume revisions don't overlap, we may need to handle it
 | |
| 		historyInfo[history.Revision] = history
 | |
| 	}
 | |
| 	if len(historyInfo) == 0 {
 | |
| 		return "No rollout history found.", nil
 | |
| 	}
 | |
| 
 | |
| 	// Print details of a specific revision
 | |
| 	if revision > 0 {
 | |
| 		history, ok := historyInfo[revision]
 | |
| 		if !ok {
 | |
| 			return "", fmt.Errorf("unable to find the specified revision")
 | |
| 		}
 | |
| 		dsOfHistory, err := applyDaemonSetHistory(ds, history)
 | |
| 		if err != nil {
 | |
| 			return "", fmt.Errorf("unable to parse history %s", history.Name)
 | |
| 		}
 | |
| 		return printTemplate(&dsOfHistory.Spec.Template)
 | |
| 	}
 | |
| 
 | |
| 	// Print an overview of all Revisions
 | |
| 	// Sort the revisionToChangeCause map by revision
 | |
| 	revisions := make([]int64, 0, len(historyInfo))
 | |
| 	for r := range historyInfo {
 | |
| 		revisions = append(revisions, r)
 | |
| 	}
 | |
| 	sliceutil.SortInts64(revisions)
 | |
| 
 | |
| 	return tabbedString(func(out io.Writer) error {
 | |
| 		fmt.Fprintf(out, "REVISION\tCHANGE-CAUSE\n")
 | |
| 		for _, r := range revisions {
 | |
| 			// Find the change-cause of revision r
 | |
| 			changeCause := historyInfo[r].Annotations[ChangeCauseAnnotation]
 | |
| 			if len(changeCause) == 0 {
 | |
| 				changeCause = "<none>"
 | |
| 			}
 | |
| 			fmt.Fprintf(out, "%d\t%s\n", r, changeCause)
 | |
| 		}
 | |
| 		return nil
 | |
| 	})
 | |
| }
 | |
| 
 | |
| type StatefulSetHistoryViewer struct {
 | |
| 	c kubernetes.Interface
 | |
| }
 | |
| 
 | |
| // ViewHistory returns a list of the revision history of a statefulset
 | |
| // TODO: this should be a describer
 | |
| // TODO: needs to implement detailed revision view
 | |
| func (h *StatefulSetHistoryViewer) ViewHistory(namespace, name string, revision int64) (string, error) {
 | |
| 	_, history, err := statefulSetHistory(h.c.AppsV1(), namespace, name)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	if len(history) <= 0 {
 | |
| 		return "No rollout history found.", nil
 | |
| 	}
 | |
| 	revisions := make([]int64, len(history))
 | |
| 	for _, revision := range history {
 | |
| 		revisions = append(revisions, revision.Revision)
 | |
| 	}
 | |
| 	sliceutil.SortInts64(revisions)
 | |
| 
 | |
| 	return tabbedString(func(out io.Writer) error {
 | |
| 		fmt.Fprintf(out, "REVISION\n")
 | |
| 		for _, r := range revisions {
 | |
| 			fmt.Fprintf(out, "%d\n", r)
 | |
| 		}
 | |
| 		return nil
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // controlledHistories returns all ControllerRevisions in namespace that selected by selector and owned by accessor
 | |
| // TODO: Rename this to controllerHistory when other controllers have been upgraded
 | |
| func controlledHistoryV1(
 | |
| 	apps clientappsv1.AppsV1Interface,
 | |
| 	namespace string,
 | |
| 	selector labels.Selector,
 | |
| 	accessor metav1.Object) ([]*appsv1.ControllerRevision, error) {
 | |
| 	var result []*appsv1.ControllerRevision
 | |
| 	historyList, err := apps.ControllerRevisions(namespace).List(metav1.ListOptions{LabelSelector: selector.String()})
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	for i := range historyList.Items {
 | |
| 		history := historyList.Items[i]
 | |
| 		// Only add history that belongs to the API object
 | |
| 		if metav1.IsControlledBy(&history, accessor) {
 | |
| 			result = append(result, &history)
 | |
| 		}
 | |
| 	}
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // controlledHistories returns all ControllerRevisions in namespace that selected by selector and owned by accessor
 | |
| func controlledHistory(
 | |
| 	apps clientappsv1.AppsV1Interface,
 | |
| 	namespace string,
 | |
| 	selector labels.Selector,
 | |
| 	accessor metav1.Object) ([]*appsv1.ControllerRevision, error) {
 | |
| 	var result []*appsv1.ControllerRevision
 | |
| 	historyList, err := apps.ControllerRevisions(namespace).List(metav1.ListOptions{LabelSelector: selector.String()})
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	for i := range historyList.Items {
 | |
| 		history := historyList.Items[i]
 | |
| 		// Only add history that belongs to the API object
 | |
| 		if metav1.IsControlledBy(&history, accessor) {
 | |
| 			result = append(result, &history)
 | |
| 		}
 | |
| 	}
 | |
| 	return result, nil
 | |
| }
 | |
| 
 | |
| // daemonSetHistory returns the DaemonSet named name in namespace and all ControllerRevisions in its history.
 | |
| func daemonSetHistory(
 | |
| 	apps clientappsv1.AppsV1Interface,
 | |
| 	namespace, name string) (*appsv1.DaemonSet, []*appsv1.ControllerRevision, error) {
 | |
| 	ds, err := apps.DaemonSets(namespace).Get(name, metav1.GetOptions{})
 | |
| 	if err != nil {
 | |
| 		return nil, nil, fmt.Errorf("failed to retrieve DaemonSet %s: %v", name, err)
 | |
| 	}
 | |
| 	selector, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, fmt.Errorf("failed to create selector for DaemonSet %s: %v", ds.Name, err)
 | |
| 	}
 | |
| 	accessor, err := meta.Accessor(ds)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, fmt.Errorf("failed to create accessor for DaemonSet %s: %v", ds.Name, err)
 | |
| 	}
 | |
| 	history, err := controlledHistory(apps, ds.Namespace, selector, accessor)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, fmt.Errorf("unable to find history controlled by DaemonSet %s: %v", ds.Name, err)
 | |
| 	}
 | |
| 	return ds, history, nil
 | |
| }
 | |
| 
 | |
| // statefulSetHistory returns the StatefulSet named name in namespace and all ControllerRevisions in its history.
 | |
| func statefulSetHistory(
 | |
| 	apps clientappsv1.AppsV1Interface,
 | |
| 	namespace, name string) (*appsv1.StatefulSet, []*appsv1.ControllerRevision, error) {
 | |
| 	sts, err := apps.StatefulSets(namespace).Get(name, metav1.GetOptions{})
 | |
| 	if err != nil {
 | |
| 		return nil, nil, fmt.Errorf("failed to retrieve Statefulset %s: %s", name, err.Error())
 | |
| 	}
 | |
| 	selector, err := metav1.LabelSelectorAsSelector(sts.Spec.Selector)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, fmt.Errorf("failed to create selector for StatefulSet %s: %s", name, err.Error())
 | |
| 	}
 | |
| 	accessor, err := meta.Accessor(sts)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, fmt.Errorf("failed to obtain accessor for StatefulSet %s: %s", name, err.Error())
 | |
| 	}
 | |
| 	history, err := controlledHistoryV1(apps, namespace, selector, accessor)
 | |
| 	if err != nil {
 | |
| 		return nil, nil, fmt.Errorf("unable to find history controlled by StatefulSet %s: %v", name, err)
 | |
| 	}
 | |
| 	return sts, history, nil
 | |
| }
 | |
| 
 | |
| // applyDaemonSetHistory returns a specific revision of DaemonSet by applying the given history to a copy of the given DaemonSet
 | |
| func applyDaemonSetHistory(ds *appsv1.DaemonSet, history *appsv1.ControllerRevision) (*appsv1.DaemonSet, error) {
 | |
| 	clone := ds.DeepCopy()
 | |
| 	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
 | |
| func tabbedString(f func(io.Writer) error) (string, error) {
 | |
| 	out := new(tabwriter.Writer)
 | |
| 	buf := &bytes.Buffer{}
 | |
| 	out.Init(buf, 0, 8, 2, ' ', 0)
 | |
| 
 | |
| 	err := f(out)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	out.Flush()
 | |
| 	str := string(buf.String())
 | |
| 	return str, nil
 | |
| }
 | |
| 
 | |
| // getChangeCause returns the change-cause annotation of the input object
 | |
| func getChangeCause(obj runtime.Object) string {
 | |
| 	accessor, err := meta.Accessor(obj)
 | |
| 	if err != nil {
 | |
| 		return ""
 | |
| 	}
 | |
| 	return accessor.GetAnnotations()[ChangeCauseAnnotation]
 | |
| }
 |