mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-11-03 19:58:17 +00:00 
			
		
		
		
	Implement kubectl rollout history and undo for DaemonSet
This commit is contained in:
		@@ -41,6 +41,8 @@ IMAGE_NGINX="gcr.io/google-containers/nginx:1.7.9"
 | 
			
		||||
IMAGE_DEPLOYMENT_R1="gcr.io/google-containers/nginx:test-cmd"  # deployment-revision1.yaml
 | 
			
		||||
IMAGE_DEPLOYMENT_R2="$IMAGE_NGINX"  # deployment-revision2.yaml
 | 
			
		||||
IMAGE_PERL="gcr.io/google-containers/perl"
 | 
			
		||||
IMAGE_DAEMONSET_R1="gcr.io/google-containers/pause:2.0"
 | 
			
		||||
IMAGE_DAEMONSET_R2="gcr.io/google-containers/pause:latest"
 | 
			
		||||
 | 
			
		||||
# Expose kubectl directly for readability
 | 
			
		||||
PATH="${KUBE_OUTPUT_HOSTBIN}":$PATH
 | 
			
		||||
@@ -71,6 +73,7 @@ subjectaccessreviews="subjectaccessreviews"
 | 
			
		||||
thirdpartyresources="thirdpartyresources"
 | 
			
		||||
customresourcedefinitions="customresourcedefinitions"
 | 
			
		||||
daemonsets="daemonsets"
 | 
			
		||||
controllerrevisions="controllerrevisions"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Stops the running kubectl proxy, if there is one.
 | 
			
		||||
@@ -2868,6 +2871,39 @@ run_daemonset_tests() {
 | 
			
		||||
  kubectl apply -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}"
 | 
			
		||||
  # Template Generation should stay 1
 | 
			
		||||
  kube::test::get_object_assert 'daemonsets bind' "{{${template_generation_field}}}" '1'
 | 
			
		||||
  # Clean up
 | 
			
		||||
  kubectl delete -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
run_daemonset_history_tests() {
 | 
			
		||||
  kube::log::status "Testing kubectl(v1:daemonsets, v1:controllerrevisions)"
 | 
			
		||||
 | 
			
		||||
  ### Test rolling back a DaemonSet
 | 
			
		||||
  # Pre-condition: no DaemonSet or its pods exists
 | 
			
		||||
  kube::test::get_object_assert daemonsets "{{range.items}}{{$id_field}}:{{end}}" ''
 | 
			
		||||
  # Command
 | 
			
		||||
  # Create a DaemonSet (revision 1)
 | 
			
		||||
  kubectl apply -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}"
 | 
			
		||||
  # Rollback to revision 1 - should be no-op
 | 
			
		||||
  kubectl rollout undo daemonset --to-revision=1 "${kube_flags[@]}"
 | 
			
		||||
  kube::test::get_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R1}:"
 | 
			
		||||
  # Update the DaemonSet (revision 2)
 | 
			
		||||
  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}:"
 | 
			
		||||
  # Rollback to revision 1 with dry-run - should be no-op
 | 
			
		||||
  kubectl rollout undo daemonset --dry-run=true "${kube_flags[@]}"
 | 
			
		||||
  kube::test::get_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R2}:"
 | 
			
		||||
  # Rollback to revision 1
 | 
			
		||||
  kubectl rollout undo daemonset --to-revision=1 "${kube_flags[@]}"
 | 
			
		||||
  kube::test::wait_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R1}:"
 | 
			
		||||
  # Rollback to revision 1000000 - should fail
 | 
			
		||||
  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::get_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R1}:"
 | 
			
		||||
  # Rollback to last revision
 | 
			
		||||
  kubectl rollout undo daemonset "${kube_flags[@]}"
 | 
			
		||||
  kube::test::wait_object_assert daemonset "{{range.items}}{{$daemonset_image_field}}:{{end}}" "${IMAGE_DAEMONSET_R2}:"
 | 
			
		||||
  # Clean up
 | 
			
		||||
  kubectl delete -f hack/testdata/rollingupdate-daemonset.yaml "${kube_flags[@]}"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -3103,6 +3139,7 @@ runTests() {
 | 
			
		||||
  pdb_min_available=".spec.minAvailable"
 | 
			
		||||
  pdb_max_unavailable=".spec.maxUnavailable"
 | 
			
		||||
  template_generation_field=".spec.templateGeneration"
 | 
			
		||||
  daemonset_image_field="(index .spec.template.spec.containers 0).image"
 | 
			
		||||
 | 
			
		||||
  # Make sure "default" namespace exists.
 | 
			
		||||
  if kube::test::if_supports_resource "${namespaces}" ; then
 | 
			
		||||
@@ -3555,6 +3592,9 @@ runTests() {
 | 
			
		||||
 | 
			
		||||
  if kube::test::if_supports_resource "${daemonsets}" ; then
 | 
			
		||||
    run_daemonset_tests
 | 
			
		||||
    if kube::test::if_supports_resource "${controllerrevisions}"; then
 | 
			
		||||
      run_daemonset_history_tests
 | 
			
		||||
    fi
 | 
			
		||||
  fi
 | 
			
		||||
 | 
			
		||||
  ###########################
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										27
									
								
								hack/testdata/rollingupdate-daemonset-rv2.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								hack/testdata/rollingupdate-daemonset-rv2.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,27 @@
 | 
			
		||||
apiVersion: extensions/v1beta1
 | 
			
		||||
kind: DaemonSet
 | 
			
		||||
metadata:
 | 
			
		||||
  name: bind
 | 
			
		||||
spec:
 | 
			
		||||
  updateStrategy:
 | 
			
		||||
    type: RollingUpdate
 | 
			
		||||
    rollingUpdate:
 | 
			
		||||
      maxUnavailable: 10%
 | 
			
		||||
  template:
 | 
			
		||||
    metadata:
 | 
			
		||||
      labels:
 | 
			
		||||
        service: bind
 | 
			
		||||
    spec:
 | 
			
		||||
      affinity:
 | 
			
		||||
        podAntiAffinity:
 | 
			
		||||
          requiredDuringSchedulingIgnoredDuringExecution:
 | 
			
		||||
            - labelSelector:
 | 
			
		||||
                matchExpressions:
 | 
			
		||||
                - key: "service"
 | 
			
		||||
                  operator: "In"
 | 
			
		||||
                  values: ["bind"]
 | 
			
		||||
              topologyKey: "kubernetes.io/hostname"
 | 
			
		||||
              namespaces: []
 | 
			
		||||
      containers:
 | 
			
		||||
      - name: kubernetes-pause
 | 
			
		||||
        image: gcr.io/google-containers/pause:latest
 | 
			
		||||
@@ -48,6 +48,7 @@ go_library(
 | 
			
		||||
        "//vendor/k8s.io/apimachinery/pkg/runtime: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/runtime:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apimachinery/pkg/util/wait:go_default_library",
 | 
			
		||||
        "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
 | 
			
		||||
 
 | 
			
		||||
@@ -17,8 +17,6 @@ limitations under the License.
 | 
			
		||||
package daemon
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"sort"
 | 
			
		||||
 | 
			
		||||
@@ -29,6 +27,7 @@ import (
 | 
			
		||||
	"k8s.io/apimachinery/pkg/labels"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/runtime"
 | 
			
		||||
	intstrutil "k8s.io/apimachinery/pkg/util/intstr"
 | 
			
		||||
	"k8s.io/apimachinery/pkg/util/json"
 | 
			
		||||
	"k8s.io/kubernetes/pkg/api"
 | 
			
		||||
	"k8s.io/kubernetes/pkg/api/v1"
 | 
			
		||||
	podutil "k8s.io/kubernetes/pkg/api/v1/pod"
 | 
			
		||||
@@ -297,23 +296,18 @@ func (dsc *DaemonSetsController) controlledHistories(ds *extensions.DaemonSet) (
 | 
			
		||||
 | 
			
		||||
// Match check if ds template is semantically equal to the template stored in history
 | 
			
		||||
func Match(template *v1.PodTemplateSpec, history *apps.ControllerRevision) (bool, error) {
 | 
			
		||||
	t, err := decodeHistory(history)
 | 
			
		||||
	t, err := DecodeHistory(history)
 | 
			
		||||
	return apiequality.Semantic.DeepEqual(template, t), err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func decodeHistory(history *apps.ControllerRevision) (*v1.PodTemplateSpec, error) {
 | 
			
		||||
	raw := history.Data.Raw
 | 
			
		||||
	decoder := json.NewDecoder(bytes.NewBuffer(raw))
 | 
			
		||||
func DecodeHistory(history *apps.ControllerRevision) (*v1.PodTemplateSpec, error) {
 | 
			
		||||
	template := v1.PodTemplateSpec{}
 | 
			
		||||
	err := decoder.Decode(&template)
 | 
			
		||||
	err := json.Unmarshal(history.Data.Raw, &template)
 | 
			
		||||
	return &template, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func encodeTemplate(template *v1.PodTemplateSpec) ([]byte, error) {
 | 
			
		||||
	buffer := new(bytes.Buffer)
 | 
			
		||||
	encoder := json.NewEncoder(buffer)
 | 
			
		||||
	err := encoder.Encode(template)
 | 
			
		||||
	return buffer.Bytes(), err
 | 
			
		||||
	return json.Marshal(template)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (dsc *DaemonSetsController) snapshot(ds *extensions.DaemonSet, revision int64) (*apps.ControllerRevision, error) {
 | 
			
		||||
 
 | 
			
		||||
@@ -66,6 +66,7 @@ go_library(
 | 
			
		||||
        "//pkg/apis/policy:go_default_library",
 | 
			
		||||
        "//pkg/apis/rbac:go_default_library",
 | 
			
		||||
        "//pkg/client/clientset_generated/clientset:go_default_library",
 | 
			
		||||
        "//pkg/client/clientset_generated/clientset/typed/apps/v1beta1:go_default_library",
 | 
			
		||||
        "//pkg/client/clientset_generated/clientset/typed/core/v1:go_default_library",
 | 
			
		||||
        "//pkg/client/clientset_generated/clientset/typed/extensions/v1beta1:go_default_library",
 | 
			
		||||
        "//pkg/client/clientset_generated/internalclientset:go_default_library",
 | 
			
		||||
@@ -76,6 +77,7 @@ go_library(
 | 
			
		||||
        "//pkg/client/retry:go_default_library",
 | 
			
		||||
        "//pkg/client/unversioned:go_default_library",
 | 
			
		||||
        "//pkg/controller:go_default_library",
 | 
			
		||||
        "//pkg/controller/daemon:go_default_library",
 | 
			
		||||
        "//pkg/controller/deployment/util:go_default_library",
 | 
			
		||||
        "//pkg/credentialprovider:go_default_library",
 | 
			
		||||
        "//pkg/kubectl/resource:go_default_library",
 | 
			
		||||
@@ -88,6 +90,7 @@ 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",
 | 
			
		||||
 
 | 
			
		||||
@@ -28,15 +28,20 @@ import (
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	rollout_long = templates.LongDesc(`
 | 
			
		||||
		Manage a deployment using subcommands like "kubectl rollout undo deployment/abc"`)
 | 
			
		||||
		Manage the rollout of a resource.` + rollout_valid_resources)
 | 
			
		||||
 | 
			
		||||
	rollout_example = templates.Examples(`
 | 
			
		||||
		# Rollback to the previous deployment
 | 
			
		||||
		kubectl rollout undo deployment/abc`)
 | 
			
		||||
		kubectl rollout undo deployment/abc
 | 
			
		||||
		
 | 
			
		||||
		# Check the rollout status of a daemonset
 | 
			
		||||
		kubectl rollout status daemonset/foo`)
 | 
			
		||||
 | 
			
		||||
	rollout_valid_resources = dedent.Dedent(`
 | 
			
		||||
		Valid resource types include:
 | 
			
		||||
 | 
			
		||||
		   * deployments
 | 
			
		||||
		   * daemonsets
 | 
			
		||||
		`)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -44,7 +49,7 @@ func NewCmdRollout(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command {
 | 
			
		||||
 | 
			
		||||
	cmd := &cobra.Command{
 | 
			
		||||
		Use:     "rollout SUBCOMMAND",
 | 
			
		||||
		Short:   i18n.T("Manage a deployment rollout"),
 | 
			
		||||
		Short:   i18n.T("Manage the rollout of a resource"),
 | 
			
		||||
		Long:    rollout_long,
 | 
			
		||||
		Example: rollout_example,
 | 
			
		||||
		Run:     cmdutil.DefaultSubCommandRun(errOut),
 | 
			
		||||
@@ -54,7 +59,6 @@ func NewCmdRollout(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command {
 | 
			
		||||
	cmd.AddCommand(NewCmdRolloutPause(f, out))
 | 
			
		||||
	cmd.AddCommand(NewCmdRolloutResume(f, out))
 | 
			
		||||
	cmd.AddCommand(NewCmdRolloutUndo(f, out))
 | 
			
		||||
 | 
			
		||||
	cmd.AddCommand(NewCmdRolloutStatus(f, out))
 | 
			
		||||
 | 
			
		||||
	return cmd
 | 
			
		||||
 
 | 
			
		||||
@@ -37,14 +37,14 @@ var (
 | 
			
		||||
		# View the rollout history of a deployment
 | 
			
		||||
		kubectl rollout history deployment/abc
 | 
			
		||||
 | 
			
		||||
		# View the details of deployment revision 3
 | 
			
		||||
		kubectl rollout history deployment/abc --revision=3`)
 | 
			
		||||
		# View the details of daemonset revision 3
 | 
			
		||||
		kubectl rollout history daemonset/abc --revision=3`)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func NewCmdRolloutHistory(f cmdutil.Factory, out io.Writer) *cobra.Command {
 | 
			
		||||
	options := &resource.FilenameOptions{}
 | 
			
		||||
 | 
			
		||||
	validArgs := []string{"deployment"}
 | 
			
		||||
	validArgs := []string{"deployment", "daemonset"}
 | 
			
		||||
	argAliases := kubectl.ResourceAliases(validArgs)
 | 
			
		||||
 | 
			
		||||
	cmd := &cobra.Command{
 | 
			
		||||
 
 | 
			
		||||
@@ -53,7 +53,7 @@ var (
 | 
			
		||||
		Mark the provided resource as paused
 | 
			
		||||
 | 
			
		||||
		Paused resources will not be reconciled by a controller.
 | 
			
		||||
		Use \"kubectl rollout resume\" to resume a paused resource.
 | 
			
		||||
		Use "kubectl rollout resume" to resume a paused resource.
 | 
			
		||||
		Currently only deployments support being paused.`)
 | 
			
		||||
 | 
			
		||||
	pause_example = templates.Examples(`
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,7 @@ var (
 | 
			
		||||
func NewCmdRolloutStatus(f cmdutil.Factory, out io.Writer) *cobra.Command {
 | 
			
		||||
	options := &resource.FilenameOptions{}
 | 
			
		||||
 | 
			
		||||
	validArgs := []string{"deployment"}
 | 
			
		||||
	validArgs := []string{"deployment", "daemonset"}
 | 
			
		||||
	argAliases := kubectl.ResourceAliases(validArgs)
 | 
			
		||||
 | 
			
		||||
	cmd := &cobra.Command{
 | 
			
		||||
 
 | 
			
		||||
@@ -54,8 +54,8 @@ var (
 | 
			
		||||
		# Rollback to the previous deployment
 | 
			
		||||
		kubectl rollout undo deployment/abc
 | 
			
		||||
 | 
			
		||||
		# Rollback to deployment revision 3
 | 
			
		||||
		kubectl rollout undo deployment/abc --to-revision=3
 | 
			
		||||
		# Rollback to daemonset revision 3
 | 
			
		||||
		kubectl rollout undo daemonset/abc --to-revision=3
 | 
			
		||||
 | 
			
		||||
		# Rollback to the previous deployment with dry-run
 | 
			
		||||
		kubectl rollout undo --dry-run=true deployment/abc`)
 | 
			
		||||
@@ -64,7 +64,7 @@ var (
 | 
			
		||||
func NewCmdRolloutUndo(f cmdutil.Factory, out io.Writer) *cobra.Command {
 | 
			
		||||
	options := &UndoOptions{}
 | 
			
		||||
 | 
			
		||||
	validArgs := []string{"deployment"}
 | 
			
		||||
	validArgs := []string{"deployment", "daemonset"}
 | 
			
		||||
	argAliases := kubectl.ResourceAliases(validArgs)
 | 
			
		||||
 | 
			
		||||
	cmd := &cobra.Command{
 | 
			
		||||
 
 | 
			
		||||
@@ -29,8 +29,11 @@ import (
 | 
			
		||||
	"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"
 | 
			
		||||
	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"
 | 
			
		||||
@@ -49,6 +52,8 @@ func HistoryViewerFor(kind schema.GroupKind, c clientset.Interface) (HistoryView
 | 
			
		||||
	switch kind {
 | 
			
		||||
	case extensions.Kind("Deployment"), apps.Kind("Deployment"):
 | 
			
		||||
		return &DeploymentHistoryViewer{c}, nil
 | 
			
		||||
	case extensions.Kind("DaemonSet"):
 | 
			
		||||
		return &DaemonSetHistoryViewer{c}, nil
 | 
			
		||||
	}
 | 
			
		||||
	return nil, fmt.Errorf("no history viewer has been implemented for %q", kind)
 | 
			
		||||
}
 | 
			
		||||
@@ -100,14 +105,7 @@ func (h *DeploymentHistoryViewer) ViewHistory(namespace, name string, revision i
 | 
			
		||||
		if !ok {
 | 
			
		||||
			return "", fmt.Errorf("unable to find the specified revision")
 | 
			
		||||
		}
 | 
			
		||||
		buf := bytes.NewBuffer([]byte{})
 | 
			
		||||
		internalTemplate := &api.PodTemplateSpec{}
 | 
			
		||||
		if err := v1.Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(template, internalTemplate, nil); err != nil {
 | 
			
		||||
			return "", fmt.Errorf("failed to convert podtemplate, %v", err)
 | 
			
		||||
		}
 | 
			
		||||
		w := printersinternal.NewPrefixWriter(buf)
 | 
			
		||||
		printersinternal.DescribePodTemplate(internalTemplate, w)
 | 
			
		||||
		return buf.String(), nil
 | 
			
		||||
		return printTemplate(template)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Sort the revisionToChangeCause map by revision
 | 
			
		||||
@@ -131,6 +129,101 @@ func (h *DeploymentHistoryViewer) ViewHistory(namespace, name string, revision i
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func printTemplate(template *v1.PodTemplateSpec) (string, error) {
 | 
			
		||||
	buf := bytes.NewBuffer([]byte{})
 | 
			
		||||
	internalTemplate := &api.PodTemplateSpec{}
 | 
			
		||||
	if err := v1.Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(template, internalTemplate, nil); err != nil {
 | 
			
		||||
		return "", fmt.Errorf("failed to convert podtemplate, %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	w := printersinternal.NewPrefixWriter(buf)
 | 
			
		||||
	printersinternal.DescribePodTemplate(internalTemplate, w)
 | 
			
		||||
	return buf.String(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DaemonSetHistoryViewer struct {
 | 
			
		||||
	c clientset.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, err := h.c.Extensions().DaemonSets(namespace).Get(name, metav1.GetOptions{})
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
	historyInfo := make(map[int64]*appsv1beta1.ControllerRevision)
 | 
			
		||||
	for _, history := range allHistory {
 | 
			
		||||
		// 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")
 | 
			
		||||
		}
 | 
			
		||||
		template, err := daemon.DecodeHistory(history)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", fmt.Errorf("unable to decode history %s", history.Name)
 | 
			
		||||
		}
 | 
			
		||||
		return printTemplate(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
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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) {
 | 
			
		||||
	var result []*appsv1beta1.ControllerRevision
 | 
			
		||||
	selector, err := metav1.LabelSelectorAsSelector(ds.Spec.Selector)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	versionedClient := versionedClientsetForDaemonSet(c)
 | 
			
		||||
	historyList, err := versionedClient.AppsV1beta1().ControllerRevisions(ds.Namespace).List(metav1.ListOptions{LabelSelector: selector.String()})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	for i := range historyList.Items {
 | 
			
		||||
		history := historyList.Items[i]
 | 
			
		||||
		// Skip history that doesn't belong to the DaemonSet
 | 
			
		||||
		if controllerRef := controller.GetControllerOf(&history); controllerRef == nil || controllerRef.UID != ds.UID {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		result = append(result, &history)
 | 
			
		||||
	}
 | 
			
		||||
	return result, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: copied here until this becomes a describer
 | 
			
		||||
func tabbedString(f func(io.Writer) error) (string, error) {
 | 
			
		||||
	out := new(tabwriter.Writer)
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,10 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"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"
 | 
			
		||||
@@ -30,14 +32,22 @@ import (
 | 
			
		||||
	"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"
 | 
			
		||||
	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"
 | 
			
		||||
	sliceutil "k8s.io/kubernetes/pkg/util/slice"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
@@ -47,6 +57,8 @@ func RollbackerFor(kind schema.GroupKind, c clientset.Interface) (Rollbacker, er
 | 
			
		||||
	switch kind {
 | 
			
		||||
	case extensions.Kind("Deployment"), apps.Kind("Deployment"):
 | 
			
		||||
		return &DeploymentRollbacker{c}, nil
 | 
			
		||||
	case extensions.Kind("DaemonSet"):
 | 
			
		||||
		return &DaemonSetRollbacker{c}, nil
 | 
			
		||||
	}
 | 
			
		||||
	return nil, fmt.Errorf("no rollbacker has been implemented for %q", kind)
 | 
			
		||||
}
 | 
			
		||||
@@ -126,9 +138,9 @@ func isRollbackEvent(e *api.Event) (bool, string) {
 | 
			
		||||
	for _, reason := range rollbackEventReasons {
 | 
			
		||||
		if e.Reason == reason {
 | 
			
		||||
			if reason == deploymentutil.RollbackDone {
 | 
			
		||||
				return true, "rolled back"
 | 
			
		||||
				return true, rollbackSuccess
 | 
			
		||||
			}
 | 
			
		||||
			return true, fmt.Sprintf("skipped rollback (%s: %s)", e.Reason, e.Message)
 | 
			
		||||
			return true, fmt.Sprintf("%s (%s: %s)", rollbackSkipped, e.Reason, e.Message)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return false, ""
 | 
			
		||||
@@ -165,7 +177,7 @@ func simpleDryRun(deployment *extensions.Deployment, c clientset.Interface, toRe
 | 
			
		||||
	if toRevision > 0 {
 | 
			
		||||
		template, ok := revisionToSpec[toRevision]
 | 
			
		||||
		if !ok {
 | 
			
		||||
			return "", fmt.Errorf("unable to find specified revision")
 | 
			
		||||
			return "", revisionNotFoundErr(toRevision)
 | 
			
		||||
		}
 | 
			
		||||
		buf := bytes.NewBuffer([]byte{})
 | 
			
		||||
		internalTemplate := &api.PodTemplateSpec{}
 | 
			
		||||
@@ -195,3 +207,108 @@ func simpleDryRun(deployment *extensions.Deployment, c clientset.Interface, toRe
 | 
			
		||||
	printersinternal.DescribePodTemplate(internalTemplate, w)
 | 
			
		||||
	return buf.String(), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DaemonSetRollbacker struct {
 | 
			
		||||
	c clientset.Interface
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (r *DaemonSetRollbacker) Rollback(obj runtime.Object, updatedAnnotations map[string]string, toRevision int64, dryRun bool) (string, error) {
 | 
			
		||||
	if toRevision < 0 {
 | 
			
		||||
		return "", revisionNotFoundErr(toRevision)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ds, ok := obj.(*extensions.DaemonSet)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return "", fmt.Errorf("passed object is not a DaemonSet: %#v", obj)
 | 
			
		||||
	}
 | 
			
		||||
	allHistory, err := controlledHistories(r.c, ds)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", fmt.Errorf("unable to find history controlled by DaemonSet %s: %v", ds.Name, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if toRevision == 0 && len(allHistory) <= 1 {
 | 
			
		||||
		return "", fmt.Errorf("no last revision to roll back to")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Find the history to rollback to
 | 
			
		||||
	var toHistory *appsv1beta1.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
 | 
			
		||||
				toHistory = h
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if toHistory == nil {
 | 
			
		||||
		return "", revisionNotFoundErr(toRevision)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Get the template of the history to rollback to
 | 
			
		||||
	toTemplate, err := getInternalTemplate(toHistory)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if dryRun {
 | 
			
		||||
		content := bytes.NewBuffer([]byte{})
 | 
			
		||||
		w := printersinternal.NewPrefixWriter(content)
 | 
			
		||||
		printersinternal.DescribePodTemplate(toTemplate, 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
 | 
			
		||||
	}
 | 
			
		||||
	if skipUpdate {
 | 
			
		||||
		return fmt.Sprintf("%s (current template already matches revision %d)", rollbackSkipped, toRevision), nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return rollbackSuccess, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 []*appsv1beta1.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
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ package kubectl
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	externalclientset "k8s.io/kubernetes/pkg/client/clientset_generated/clientset"
 | 
			
		||||
	apps "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/apps/v1beta1"
 | 
			
		||||
	core "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/core/v1"
 | 
			
		||||
	extensions "k8s.io/kubernetes/pkg/client/clientset_generated/clientset/typed/extensions/v1beta1"
 | 
			
		||||
	internalclientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
 | 
			
		||||
@@ -32,3 +33,12 @@ func versionedClientsetForDeployment(internalClient internalclientset.Interface)
 | 
			
		||||
		ExtensionsV1beta1Client: extensions.New(internalClient.Extensions().RESTClient()),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func versionedClientsetForDaemonSet(internalClient internalclientset.Interface) externalclientset.Interface {
 | 
			
		||||
	if internalClient == nil {
 | 
			
		||||
		return &externalclientset.Clientset{}
 | 
			
		||||
	}
 | 
			
		||||
	return &externalclientset.Clientset{
 | 
			
		||||
		AppsV1beta1Client: apps.New(internalClient.Apps().RESTClient()),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user