From 26bf978c07a90d1e4f21d1c02a8de7eb01c3ad3f Mon Sep 17 00:00:00 2001 From: Kenneth Owens Date: Tue, 10 Oct 2017 14:30:02 -0700 Subject: [PATCH] Promotes the StatefulSet, ControllerRevision, Deployment, and ReplicaSet kinds to the apps/v1 group version. --- cmd/kube-apiserver/app/aggregator.go | 2 +- pkg/api/testing/defaulting_test.go | 6 + pkg/apis/apps/v1/conversion.go | 328 ++++++++++ pkg/apis/apps/v1/conversion_test.go | 539 +++++++++++++++++ pkg/apis/apps/v1/defaults.go | 78 +++ pkg/apis/apps/v1/defaults_test.go | 377 ++++++++++++ pkg/registry/apps/rest/storage_apps.go | 19 + .../extensions/deployment/strategy.go | 3 +- .../extensions/replicaset/strategy.go | 3 +- staging/src/k8s.io/api/apps/v1/register.go | 8 + staging/src/k8s.io/api/apps/v1/types.go | 568 ++++++++++++++++++ .../etcd/etcd_storage_path_test.go | 20 + 12 files changed, 1948 insertions(+), 3 deletions(-) diff --git a/cmd/kube-apiserver/app/aggregator.go b/cmd/kube-apiserver/app/aggregator.go index 68375487f00..ed50c5511ac 100644 --- a/cmd/kube-apiserver/app/aggregator.go +++ b/cmd/kube-apiserver/app/aggregator.go @@ -203,7 +203,7 @@ var apiVersionPriorities = map[schema.GroupVersion]priority{ {Group: "extensions", Version: "v1beta1"}: {group: 17900, version: 1}, // to my knowledge, nothing below here collides {Group: "apps", Version: "v1beta1"}: {group: 17800, version: 1}, - {Group: "apps", Version: "v1beta2"}: {group: 17800, version: 1}, + {Group: "apps", Version: "v1beta2"}: {group: 17800, version: 9}, {Group: "apps", Version: "v1"}: {group: 17800, version: 15}, {Group: "authentication.k8s.io", Version: "v1"}: {group: 17700, version: 15}, {Group: "authentication.k8s.io", Version: "v1beta1"}: {group: 17700, version: 9}, diff --git a/pkg/api/testing/defaulting_test.go b/pkg/api/testing/defaulting_test.go index 6e1bff7e28a..306c28d2e40 100644 --- a/pkg/api/testing/defaulting_test.go +++ b/pkg/api/testing/defaulting_test.go @@ -74,6 +74,8 @@ func TestDefaulting(t *testing.T) { {Group: "apps", Version: "v1beta1", Kind: "StatefulSetList"}: {}, {Group: "apps", Version: "v1beta2", Kind: "StatefulSet"}: {}, {Group: "apps", Version: "v1beta2", Kind: "StatefulSetList"}: {}, + {Group: "apps", Version: "v1", Kind: "StatefulSet"}: {}, + {Group: "apps", Version: "v1", Kind: "StatefulSetList"}: {}, {Group: "autoscaling", Version: "v1", Kind: "HorizontalPodAutoscaler"}: {}, {Group: "autoscaling", Version: "v1", Kind: "HorizontalPodAutoscalerList"}: {}, {Group: "autoscaling", Version: "v2beta1", Kind: "HorizontalPodAutoscaler"}: {}, @@ -107,10 +109,14 @@ func TestDefaulting(t *testing.T) { {Group: "apps", Version: "v1beta1", Kind: "DeploymentList"}: {}, {Group: "apps", Version: "v1beta2", Kind: "Deployment"}: {}, {Group: "apps", Version: "v1beta2", Kind: "DeploymentList"}: {}, + {Group: "apps", Version: "v1", Kind: "Deployment"}: {}, + {Group: "apps", Version: "v1", Kind: "DeploymentList"}: {}, {Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicy"}: {}, {Group: "extensions", Version: "v1beta1", Kind: "PodSecurityPolicyList"}: {}, {Group: "apps", Version: "v1beta2", Kind: "ReplicaSet"}: {}, {Group: "apps", Version: "v1beta2", Kind: "ReplicaSetList"}: {}, + {Group: "apps", Version: "v1", Kind: "ReplicaSet"}: {}, + {Group: "apps", Version: "v1", Kind: "ReplicaSetList"}: {}, {Group: "extensions", Version: "v1beta1", Kind: "ReplicaSet"}: {}, {Group: "extensions", Version: "v1beta1", Kind: "ReplicaSetList"}: {}, {Group: "extensions", Version: "v1beta1", Kind: "NetworkPolicy"}: {}, diff --git a/pkg/apis/apps/v1/conversion.go b/pkg/apis/apps/v1/conversion.go index 2e171964736..ff6451adaaf 100644 --- a/pkg/apis/apps/v1/conversion.go +++ b/pkg/apis/apps/v1/conversion.go @@ -17,13 +17,18 @@ limitations under the License. package v1 import ( + "fmt" "strconv" appsv1 "k8s.io/api/apps/v1" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/conversion" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/kubernetes/pkg/api" k8s_api_v1 "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/extensions" ) @@ -33,14 +38,33 @@ func addConversionFuncs(scheme *runtime.Scheme) error { // it, but a plain int32 is more convenient in the internal type. These // functions are the same as the autogenerated ones in every other way. err := scheme.AddConversionFuncs( + Convert_v1_StatefulSetSpec_To_apps_StatefulSetSpec, + Convert_apps_StatefulSetSpec_To_v1_StatefulSetSpec, + Convert_v1_StatefulSetUpdateStrategy_To_apps_StatefulSetUpdateStrategy, + Convert_apps_StatefulSetUpdateStrategy_To_v1_StatefulSetUpdateStrategy, Convert_extensions_RollingUpdateDaemonSet_To_v1_RollingUpdateDaemonSet, Convert_v1_RollingUpdateDaemonSet_To_extensions_RollingUpdateDaemonSet, + Convert_v1_StatefulSetStatus_To_apps_StatefulSetStatus, + Convert_apps_StatefulSetStatus_To_v1_StatefulSetStatus, + Convert_v1_Deployment_To_extensions_Deployment, + Convert_extensions_Deployment_To_v1_Deployment, Convert_extensions_DaemonSet_To_v1_DaemonSet, Convert_v1_DaemonSet_To_extensions_DaemonSet, Convert_extensions_DaemonSetSpec_To_v1_DaemonSetSpec, Convert_v1_DaemonSetSpec_To_extensions_DaemonSetSpec, Convert_extensions_DaemonSetUpdateStrategy_To_v1_DaemonSetUpdateStrategy, Convert_v1_DaemonSetUpdateStrategy_To_extensions_DaemonSetUpdateStrategy, + // extensions + // TODO: below conversions should be dropped in favor of auto-generated + // ones, see https://github.com/kubernetes/kubernetes/issues/39865 + Convert_v1_DeploymentSpec_To_extensions_DeploymentSpec, + Convert_extensions_DeploymentSpec_To_v1_DeploymentSpec, + Convert_v1_DeploymentStrategy_To_extensions_DeploymentStrategy, + Convert_extensions_DeploymentStrategy_To_v1_DeploymentStrategy, + Convert_v1_RollingUpdateDeployment_To_extensions_RollingUpdateDeployment, + Convert_extensions_RollingUpdateDeployment_To_v1_RollingUpdateDeployment, + Convert_extensions_ReplicaSetSpec_To_v1_ReplicaSetSpec, + Convert_v1_ReplicaSetSpec_To_extensions_ReplicaSetSpec, ) if err != nil { return err @@ -48,6 +72,150 @@ func addConversionFuncs(scheme *runtime.Scheme) error { return nil } +func Convert_v1_DeploymentSpec_To_extensions_DeploymentSpec(in *appsv1.DeploymentSpec, out *extensions.DeploymentSpec, s conversion.Scope) error { + if in.Replicas != nil { + out.Replicas = *in.Replicas + } + out.Selector = in.Selector + if err := k8s_api_v1.Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { + return err + } + if err := Convert_v1_DeploymentStrategy_To_extensions_DeploymentStrategy(&in.Strategy, &out.Strategy, s); err != nil { + return err + } + out.RevisionHistoryLimit = in.RevisionHistoryLimit + out.MinReadySeconds = in.MinReadySeconds + out.Paused = in.Paused + if in.ProgressDeadlineSeconds != nil { + out.ProgressDeadlineSeconds = new(int32) + *out.ProgressDeadlineSeconds = *in.ProgressDeadlineSeconds + } + return nil +} + +func Convert_extensions_DeploymentSpec_To_v1_DeploymentSpec(in *extensions.DeploymentSpec, out *appsv1.DeploymentSpec, s conversion.Scope) error { + out.Replicas = &in.Replicas + out.Selector = in.Selector + if err := k8s_api_v1.Convert_api_PodTemplateSpec_To_v1_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { + return err + } + if err := Convert_extensions_DeploymentStrategy_To_v1_DeploymentStrategy(&in.Strategy, &out.Strategy, s); err != nil { + return err + } + if in.RevisionHistoryLimit != nil { + out.RevisionHistoryLimit = new(int32) + *out.RevisionHistoryLimit = int32(*in.RevisionHistoryLimit) + } + out.MinReadySeconds = int32(in.MinReadySeconds) + out.Paused = in.Paused + if in.ProgressDeadlineSeconds != nil { + out.ProgressDeadlineSeconds = new(int32) + *out.ProgressDeadlineSeconds = *in.ProgressDeadlineSeconds + } + return nil +} + +func Convert_extensions_DeploymentStrategy_To_v1_DeploymentStrategy(in *extensions.DeploymentStrategy, out *appsv1.DeploymentStrategy, s conversion.Scope) error { + out.Type = appsv1.DeploymentStrategyType(in.Type) + if in.RollingUpdate != nil { + out.RollingUpdate = new(appsv1.RollingUpdateDeployment) + if err := Convert_extensions_RollingUpdateDeployment_To_v1_RollingUpdateDeployment(in.RollingUpdate, out.RollingUpdate, s); err != nil { + return err + } + } else { + out.RollingUpdate = nil + } + return nil +} + +func Convert_v1_DeploymentStrategy_To_extensions_DeploymentStrategy(in *appsv1.DeploymentStrategy, out *extensions.DeploymentStrategy, s conversion.Scope) error { + out.Type = extensions.DeploymentStrategyType(in.Type) + if in.RollingUpdate != nil { + out.RollingUpdate = new(extensions.RollingUpdateDeployment) + if err := Convert_v1_RollingUpdateDeployment_To_extensions_RollingUpdateDeployment(in.RollingUpdate, out.RollingUpdate, s); err != nil { + return err + } + } else { + out.RollingUpdate = nil + } + return nil +} + +func Convert_v1_RollingUpdateDeployment_To_extensions_RollingUpdateDeployment(in *appsv1.RollingUpdateDeployment, out *extensions.RollingUpdateDeployment, s conversion.Scope) error { + if err := s.Convert(in.MaxUnavailable, &out.MaxUnavailable, 0); err != nil { + return err + } + if err := s.Convert(in.MaxSurge, &out.MaxSurge, 0); err != nil { + return err + } + return nil +} + +func Convert_extensions_RollingUpdateDeployment_To_v1_RollingUpdateDeployment(in *extensions.RollingUpdateDeployment, out *appsv1.RollingUpdateDeployment, s conversion.Scope) error { + if out.MaxUnavailable == nil { + out.MaxUnavailable = &intstr.IntOrString{} + } + if err := s.Convert(&in.MaxUnavailable, out.MaxUnavailable, 0); err != nil { + return err + } + if out.MaxSurge == nil { + out.MaxSurge = &intstr.IntOrString{} + } + if err := s.Convert(&in.MaxSurge, out.MaxSurge, 0); err != nil { + return err + } + return nil +} + +func Convert_v1_Deployment_To_extensions_Deployment(in *appsv1.Deployment, out *extensions.Deployment, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_v1_DeploymentSpec_To_extensions_DeploymentSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + + // Copy annotation to deprecated rollbackTo field for roundtrip + // TODO: remove this conversion after we delete extensions/v1beta1 and apps/v1beta1 Deployment + if revision, _ := in.Annotations[appsv1.DeprecatedRollbackTo]; revision != "" { + if revision64, err := strconv.ParseInt(revision, 10, 64); err != nil { + return fmt.Errorf("failed to parse annotation[%s]=%s as int64: %v", appsv1.DeprecatedRollbackTo, revision, err) + } else { + out.Spec.RollbackTo = new(extensions.RollbackConfig) + out.Spec.RollbackTo.Revision = revision64 + } + delete(out.Annotations, appsv1.DeprecatedRollbackTo) + } else { + out.Spec.RollbackTo = nil + } + + if err := Convert_v1_DeploymentStatus_To_extensions_DeploymentStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +func Convert_extensions_Deployment_To_v1_Deployment(in *extensions.Deployment, out *appsv1.Deployment, s conversion.Scope) error { + out.ObjectMeta = in.ObjectMeta + if err := Convert_extensions_DeploymentSpec_To_v1_DeploymentSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + + // Copy deprecated rollbackTo field to annotation for roundtrip + // TODO: remove this conversion after we delete extensions/v1beta1 and apps/v1beta1 Deployment + if in.Spec.RollbackTo != nil { + if out.Annotations == nil { + out.Annotations = make(map[string]string) + } + out.Annotations[appsv1.DeprecatedRollbackTo] = strconv.FormatInt(in.Spec.RollbackTo.Revision, 10) + } else { + delete(out.Annotations, appsv1.DeprecatedRollbackTo) + } + + if err := Convert_extensions_DeploymentStatus_To_v1_DeploymentStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + func Convert_extensions_RollingUpdateDaemonSet_To_v1_RollingUpdateDaemonSet(in *extensions.RollingUpdateDaemonSet, out *appsv1.RollingUpdateDaemonSet, s conversion.Scope) error { if out.MaxUnavailable == nil { out.MaxUnavailable = &intstr.IntOrString{} @@ -156,3 +324,163 @@ func Convert_v1_DaemonSetUpdateStrategy_To_extensions_DaemonSetUpdateStrategy(in } return nil } + +func Convert_extensions_ReplicaSetSpec_To_v1_ReplicaSetSpec(in *extensions.ReplicaSetSpec, out *appsv1.ReplicaSetSpec, s conversion.Scope) error { + out.Replicas = new(int32) + *out.Replicas = int32(in.Replicas) + out.MinReadySeconds = in.MinReadySeconds + out.Selector = in.Selector + if err := k8s_api_v1.Convert_api_PodTemplateSpec_To_v1_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { + return err + } + return nil +} + +func Convert_v1_ReplicaSetSpec_To_extensions_ReplicaSetSpec(in *appsv1.ReplicaSetSpec, out *extensions.ReplicaSetSpec, s conversion.Scope) error { + if in.Replicas != nil { + out.Replicas = *in.Replicas + } + out.MinReadySeconds = in.MinReadySeconds + out.Selector = in.Selector + if err := k8s_api_v1.Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { + return err + } + return nil +} + +func Convert_v1_StatefulSetSpec_To_apps_StatefulSetSpec(in *appsv1.StatefulSetSpec, out *apps.StatefulSetSpec, s conversion.Scope) error { + if in.Replicas != nil { + out.Replicas = *in.Replicas + } + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(metav1.LabelSelector) + if err := s.Convert(*in, *out, 0); err != nil { + return err + } + } else { + out.Selector = nil + } + if err := k8s_api_v1.Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { + return err + } + if in.VolumeClaimTemplates != nil { + in, out := &in.VolumeClaimTemplates, &out.VolumeClaimTemplates + *out = make([]api.PersistentVolumeClaim, len(*in)) + for i := range *in { + if err := s.Convert(&(*in)[i], &(*out)[i], 0); err != nil { + return err + } + } + } else { + out.VolumeClaimTemplates = nil + } + if err := Convert_v1_StatefulSetUpdateStrategy_To_apps_StatefulSetUpdateStrategy(&in.UpdateStrategy, &out.UpdateStrategy, s); err != nil { + return err + } + if in.RevisionHistoryLimit != nil { + out.RevisionHistoryLimit = new(int32) + *out.RevisionHistoryLimit = *in.RevisionHistoryLimit + } else { + out.RevisionHistoryLimit = nil + } + out.ServiceName = in.ServiceName + out.PodManagementPolicy = apps.PodManagementPolicyType(in.PodManagementPolicy) + return nil +} + +func Convert_apps_StatefulSetSpec_To_v1_StatefulSetSpec(in *apps.StatefulSetSpec, out *appsv1.StatefulSetSpec, s conversion.Scope) error { + out.Replicas = new(int32) + *out.Replicas = in.Replicas + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(metav1.LabelSelector) + if err := s.Convert(*in, *out, 0); err != nil { + return err + } + } else { + out.Selector = nil + } + if err := k8s_api_v1.Convert_api_PodTemplateSpec_To_v1_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { + return err + } + if in.VolumeClaimTemplates != nil { + in, out := &in.VolumeClaimTemplates, &out.VolumeClaimTemplates + *out = make([]v1.PersistentVolumeClaim, len(*in)) + for i := range *in { + if err := s.Convert(&(*in)[i], &(*out)[i], 0); err != nil { + return err + } + } + } else { + out.VolumeClaimTemplates = nil + } + if in.RevisionHistoryLimit != nil { + out.RevisionHistoryLimit = new(int32) + *out.RevisionHistoryLimit = *in.RevisionHistoryLimit + } else { + out.RevisionHistoryLimit = nil + } + out.ServiceName = in.ServiceName + out.PodManagementPolicy = appsv1.PodManagementPolicyType(in.PodManagementPolicy) + if err := Convert_apps_StatefulSetUpdateStrategy_To_v1_StatefulSetUpdateStrategy(&in.UpdateStrategy, &out.UpdateStrategy, s); err != nil { + return err + } + return nil +} + +func Convert_v1_StatefulSetUpdateStrategy_To_apps_StatefulSetUpdateStrategy(in *appsv1.StatefulSetUpdateStrategy, out *apps.StatefulSetUpdateStrategy, s conversion.Scope) error { + out.Type = apps.StatefulSetUpdateStrategyType(in.Type) + if in.RollingUpdate != nil { + out.RollingUpdate = new(apps.RollingUpdateStatefulSetStrategy) + out.RollingUpdate.Partition = *in.RollingUpdate.Partition + } else { + out.RollingUpdate = nil + } + return nil +} + +func Convert_apps_StatefulSetUpdateStrategy_To_v1_StatefulSetUpdateStrategy(in *apps.StatefulSetUpdateStrategy, out *appsv1.StatefulSetUpdateStrategy, s conversion.Scope) error { + out.Type = appsv1.StatefulSetUpdateStrategyType(in.Type) + if in.RollingUpdate != nil { + out.RollingUpdate = new(appsv1.RollingUpdateStatefulSetStrategy) + out.RollingUpdate.Partition = new(int32) + *out.RollingUpdate.Partition = in.RollingUpdate.Partition + } else { + out.RollingUpdate = nil + } + return nil +} + +func Convert_v1_StatefulSetStatus_To_apps_StatefulSetStatus(in *appsv1.StatefulSetStatus, out *apps.StatefulSetStatus, s conversion.Scope) error { + out.ObservedGeneration = new(int64) + *out.ObservedGeneration = in.ObservedGeneration + out.Replicas = in.Replicas + out.ReadyReplicas = in.ReadyReplicas + out.CurrentReplicas = in.CurrentReplicas + out.UpdatedReplicas = in.UpdatedReplicas + out.CurrentRevision = in.CurrentRevision + out.UpdateRevision = in.UpdateRevision + if in.CollisionCount != nil { + out.CollisionCount = new(int32) + *out.CollisionCount = *in.CollisionCount + } + return nil +} + +func Convert_apps_StatefulSetStatus_To_v1_StatefulSetStatus(in *apps.StatefulSetStatus, out *appsv1.StatefulSetStatus, s conversion.Scope) error { + if in.ObservedGeneration != nil { + out.ObservedGeneration = *in.ObservedGeneration + } + out.Replicas = in.Replicas + out.ReadyReplicas = in.ReadyReplicas + out.CurrentReplicas = in.CurrentReplicas + out.UpdatedReplicas = in.UpdatedReplicas + out.CurrentRevision = in.CurrentRevision + out.UpdateRevision = in.UpdateRevision + if in.CollisionCount != nil { + out.CollisionCount = new(int32) + *out.CollisionCount = *in.CollisionCount + } + return nil +} diff --git a/pkg/apis/apps/v1/conversion_test.go b/pkg/apis/apps/v1/conversion_test.go index b0ce083fc3c..c3ea72ecf3f 100644 --- a/pkg/apis/apps/v1/conversion_test.go +++ b/pkg/apis/apps/v1/conversion_test.go @@ -20,13 +20,211 @@ import ( "testing" appsv1 "k8s.io/api/apps/v1" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/legacyscheme" + "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/extensions" apiequality "k8s.io/apimachinery/pkg/api/equality" ) +func TestV12StatefulSetSpecConversion(t *testing.T) { + replicas := newInt32(2) + selector := &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}} + appsv1Template := v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: v1.PodSpec{ + RestartPolicy: v1.RestartPolicy("bar"), + SecurityContext: new(v1.PodSecurityContext), + }, + } + apiTemplate := api.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Name: "foo"}, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicy("bar"), + SecurityContext: new(api.PodSecurityContext), + }, + } + testcases := map[string]struct { + stsSpec1 *apps.StatefulSetSpec + stsSepc2 *appsv1.StatefulSetSpec + }{ + "StatefulSetSpec Conversion 1": { + stsSpec1: &apps.StatefulSetSpec{ + Replicas: *replicas, + Template: apiTemplate, + }, + stsSepc2: &appsv1.StatefulSetSpec{ + Replicas: replicas, + Template: appsv1Template, + }, + }, + "StatefulSetSpec Conversion 2": { + stsSpec1: &apps.StatefulSetSpec{ + Replicas: *replicas, + Selector: selector, + Template: apiTemplate, + ServiceName: "foo", + PodManagementPolicy: apps.PodManagementPolicyType("bar"), + }, + stsSepc2: &appsv1.StatefulSetSpec{ + Replicas: replicas, + Selector: selector, + Template: appsv1Template, + ServiceName: "foo", + PodManagementPolicy: appsv1.PodManagementPolicyType("bar"), + }, + }, + } + + for k, tc := range testcases { + // apps -> appsv1 + internal1 := &appsv1.StatefulSetSpec{} + if err := legacyscheme.Scheme.Convert(tc.stsSpec1, internal1, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", k, "from extensions to appsv1", err) + } + + if !apiequality.Semantic.DeepEqual(internal1, tc.stsSepc2) { + t.Errorf("%q - %q: expected\n\t%#v, got \n\t%#v", k, "from extensions to appsv1", tc.stsSepc2, internal1) + } + + // appsv1 -> apps + internal2 := &apps.StatefulSetSpec{} + if err := legacyscheme.Scheme.Convert(tc.stsSepc2, internal2, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", k, "from appsv1 to extensions", err) + } + if !apiequality.Semantic.DeepEqual(internal2, tc.stsSpec1) { + t.Errorf("%q- %q: expected\n\t%#v, got \n\t%#v", k, "from appsv1 to extensions", tc.stsSpec1, internal2) + } + } +} + +func TestV1StatefulSetStatusConversion(t *testing.T) { + observedGeneration := new(int64) + *observedGeneration = 2 + collisionCount := new(int32) + *collisionCount = 1 + testcases := map[string]struct { + stsStatus1 *apps.StatefulSetStatus + stsStatus2 *appsv1.StatefulSetStatus + }{ + "StatefulSetStatus Conversion 1": { + stsStatus1: &apps.StatefulSetStatus{ + Replicas: int32(3), + ReadyReplicas: int32(1), + CurrentReplicas: int32(3), + UpdatedReplicas: int32(3), + CurrentRevision: "12345", + UpdateRevision: "23456", + ObservedGeneration: observedGeneration, + }, + stsStatus2: &appsv1.StatefulSetStatus{ + Replicas: int32(3), + ReadyReplicas: int32(1), + CurrentReplicas: int32(3), + UpdatedReplicas: int32(3), + CurrentRevision: "12345", + UpdateRevision: "23456", + ObservedGeneration: *observedGeneration, + }, + }, + "StatefulSetStatus Conversion 2": { + stsStatus1: &apps.StatefulSetStatus{ + ObservedGeneration: observedGeneration, + Replicas: int32(3), + ReadyReplicas: int32(1), + CurrentReplicas: int32(3), + UpdatedReplicas: int32(3), + CurrentRevision: "12345", + UpdateRevision: "23456", + CollisionCount: collisionCount, + }, + stsStatus2: &appsv1.StatefulSetStatus{ + ObservedGeneration: *observedGeneration, + Replicas: int32(3), + ReadyReplicas: int32(1), + CurrentReplicas: int32(3), + UpdatedReplicas: int32(3), + CurrentRevision: "12345", + UpdateRevision: "23456", + CollisionCount: collisionCount, + }, + }, + } + + for k, tc := range testcases { + // apps -> appsv1 + internal1 := &appsv1.StatefulSetStatus{} + if err := legacyscheme.Scheme.Convert(tc.stsStatus1, internal1, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", k, "from apps to appsv1", err) + } + + if !apiequality.Semantic.DeepEqual(internal1, tc.stsStatus2) { + t.Errorf("%q - %q: expected\n\t%#v, got \n\t%#v", k, "from apps to appsv1", tc.stsStatus2, internal1) + } + + // appsv1 -> apps + internal2 := &apps.StatefulSetStatus{} + if err := legacyscheme.Scheme.Convert(tc.stsStatus2, internal2, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", k, "from appsv1 to apps", err) + } + if !apiequality.Semantic.DeepEqual(internal2, tc.stsStatus1) { + t.Errorf("%q - %q: expected\n\t%#v, got \n\t%#v", k, "from appsv1 to apps", tc.stsStatus1, internal2) + } + } +} + +func TestV1StatefulSetUpdateStrategyConversion(t *testing.T) { + partition := newInt32(2) + appsv1rollingUpdate := new(appsv1.RollingUpdateStatefulSetStrategy) + appsv1rollingUpdate.Partition = partition + appsrollingUpdate := new(apps.RollingUpdateStatefulSetStrategy) + appsrollingUpdate.Partition = *partition + testcases := map[string]struct { + stsUpdateStrategy1 *apps.StatefulSetUpdateStrategy + stsUpdateStrategy2 *appsv1.StatefulSetUpdateStrategy + }{ + "StatefulSetUpdateStrategy Conversion 1": { + stsUpdateStrategy1: &apps.StatefulSetUpdateStrategy{Type: apps.StatefulSetUpdateStrategyType("foo")}, + stsUpdateStrategy2: &appsv1.StatefulSetUpdateStrategy{Type: appsv1.StatefulSetUpdateStrategyType("foo")}, + }, + "StatefulSetUpdateStrategy Conversion 2": { + stsUpdateStrategy1: &apps.StatefulSetUpdateStrategy{ + Type: apps.StatefulSetUpdateStrategyType("foo"), + RollingUpdate: appsrollingUpdate, + }, + stsUpdateStrategy2: &appsv1.StatefulSetUpdateStrategy{ + Type: appsv1.StatefulSetUpdateStrategyType("foo"), + RollingUpdate: appsv1rollingUpdate, + }, + }, + } + + for k, tc := range testcases { + // apps -> appsv1 + internal1 := &appsv1.StatefulSetUpdateStrategy{} + if err := legacyscheme.Scheme.Convert(tc.stsUpdateStrategy1, internal1, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", "apps -> appsv1", k, err) + } + + if !apiequality.Semantic.DeepEqual(internal1, tc.stsUpdateStrategy2) { + t.Errorf("%q - %q: expected\n\t%#v, got \n\t%#v", "apps -> appsv1", k, tc.stsUpdateStrategy2, internal1) + } + + // appsv1 -> apps + internal2 := &apps.StatefulSetUpdateStrategy{} + if err := legacyscheme.Scheme.Convert(tc.stsUpdateStrategy2, internal2, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", "appsv1 -> apps", k, err) + } + if !apiequality.Semantic.DeepEqual(internal2, tc.stsUpdateStrategy1) { + t.Errorf("%q - %q: expected\n\t%#v, got \n\t%#v", "appsv1 -> apps", k, tc.stsUpdateStrategy1, internal2) + } + } +} + func TestV1RollingUpdateDaemonSetConversion(t *testing.T) { intorstr := intstr.FromInt(1) testcases := map[string]struct { @@ -59,3 +257,344 @@ func TestV1RollingUpdateDaemonSetConversion(t *testing.T) { } } } + +func TestV1DeploymentConversion(t *testing.T) { + replica := newInt32(2) + rollbackTo := new(extensions.RollbackConfig) + rollbackTo.Revision = int64(2) + testcases := map[string]struct { + deployment1 *extensions.Deployment + deployment2 *appsv1.Deployment + }{ + "Deployment Conversion 1": { + deployment1: &extensions.Deployment{ + Spec: extensions.DeploymentSpec{ + Replicas: *replica, + RollbackTo: rollbackTo, + Template: api.PodTemplateSpec{ + Spec: api.PodSpec{ + SecurityContext: new(api.PodSecurityContext), + }, + }, + }, + }, + deployment2: &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{appsv1.DeprecatedRollbackTo: "2"}, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: replica, + Template: v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + SecurityContext: new(v1.PodSecurityContext), + }, + }, + }, + }, + }, + "Deployment Conversion 2": { + deployment1: &extensions.Deployment{ + Spec: extensions.DeploymentSpec{ + Replicas: *replica, + Template: api.PodTemplateSpec{ + Spec: api.PodSpec{ + SecurityContext: new(api.PodSecurityContext), + }, + }, + }, + }, + deployment2: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: replica, + Template: v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + SecurityContext: new(v1.PodSecurityContext), + }, + }, + }, + }, + }, + } + + for k, tc := range testcases { + // extensions -> v1beta2 + internal1 := &appsv1.Deployment{} + if err := legacyscheme.Scheme.Convert(tc.deployment1, internal1, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", k, "from extensions to v1beta2", err) + } + if !apiequality.Semantic.DeepEqual(internal1, tc.deployment2) { + t.Errorf("%q - %q: expected\n\t%#v, got \n\t%#v", k, "from extensions to v1beta2", tc.deployment2, internal1) + } + + // v1beta2 -> extensions + internal2 := &extensions.Deployment{} + if err := legacyscheme.Scheme.Convert(tc.deployment2, internal2, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", k, "from v1beta2 to extensions", err) + } + if !apiequality.Semantic.DeepEqual(internal2, tc.deployment1) { + t.Errorf("%q - %q: expected\n\t%#v, got \n\t%#v", k, "from v1beta2 to extensions", tc.deployment1, internal2) + } + } +} + +func TestV1DeploymentSpecConversion(t *testing.T) { + replica := newInt32(2) + revisionHistoryLimit := newInt32(2) + progressDeadlineSeconds := newInt32(2) + + testcases := map[string]struct { + deploymentSpec1 *extensions.DeploymentSpec + deploymentSpec2 *appsv1.DeploymentSpec + }{ + "DeploymentSpec Conversion 1": { + deploymentSpec1: &extensions.DeploymentSpec{ + Replicas: *replica, + Template: api.PodTemplateSpec{ + Spec: api.PodSpec{ + SecurityContext: new(api.PodSecurityContext), + }, + }, + }, + deploymentSpec2: &appsv1.DeploymentSpec{ + Replicas: replica, + Template: v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + SecurityContext: new(v1.PodSecurityContext), + }, + }, + }, + }, + "DeploymentSpec Conversion 2": { + deploymentSpec1: &extensions.DeploymentSpec{ + Replicas: *replica, + RevisionHistoryLimit: revisionHistoryLimit, + MinReadySeconds: 2, + Paused: true, + Template: api.PodTemplateSpec{ + Spec: api.PodSpec{ + SecurityContext: new(api.PodSecurityContext), + }, + }, + }, + deploymentSpec2: &appsv1.DeploymentSpec{ + Replicas: replica, + RevisionHistoryLimit: revisionHistoryLimit, + MinReadySeconds: 2, + Paused: true, + Template: v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + SecurityContext: new(v1.PodSecurityContext), + }, + }, + }, + }, + "DeploymentSpec Conversion 3": { + deploymentSpec1: &extensions.DeploymentSpec{ + Replicas: *replica, + ProgressDeadlineSeconds: progressDeadlineSeconds, + Template: api.PodTemplateSpec{ + Spec: api.PodSpec{ + SecurityContext: new(api.PodSecurityContext), + }, + }, + }, + deploymentSpec2: &appsv1.DeploymentSpec{ + Replicas: replica, + ProgressDeadlineSeconds: progressDeadlineSeconds, + Template: v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + SecurityContext: new(v1.PodSecurityContext), + }, + }, + }, + }, + } + + // extensions -> appsv1 + for k, tc := range testcases { + internal := &appsv1.DeploymentSpec{} + if err := legacyscheme.Scheme.Convert(tc.deploymentSpec1, internal, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", "extensions -> appsv1", k, err) + } + + if !apiequality.Semantic.DeepEqual(internal, tc.deploymentSpec2) { + t.Errorf("%q - %q: expected\n\t%+v, got \n\t%+v", "extensions -> appsv1", k, tc.deploymentSpec2, internal) + } + } + + // appsv1 -> extensions + for k, tc := range testcases { + internal := &extensions.DeploymentSpec{} + if err := legacyscheme.Scheme.Convert(tc.deploymentSpec2, internal, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", "appsv1 -> extensions", k, err) + } + if !apiequality.Semantic.DeepEqual(internal, tc.deploymentSpec1) { + t.Errorf("%q - %q: expected\n\t%+v, got \n\t%+v", "appsv1 -> extensions", k, tc.deploymentSpec1, internal) + } + } + +} + +func TestV1DeploymentStrategyConversion(t *testing.T) { + maxUnavailable := intstr.FromInt(2) + maxSurge := intstr.FromInt(2) + extensionsRollingUpdate := extensions.RollingUpdateDeployment{MaxUnavailable: maxUnavailable, MaxSurge: maxSurge} + appsv1RollingUpdate := appsv1.RollingUpdateDeployment{MaxUnavailable: &maxUnavailable, MaxSurge: &maxSurge} + testcases := map[string]struct { + deploymentStrategy1 *extensions.DeploymentStrategy + deploymentStrategy2 *appsv1.DeploymentStrategy + }{ + "DeploymentStrategy Conversion 1": { + deploymentStrategy1: &extensions.DeploymentStrategy{Type: extensions.DeploymentStrategyType("foo")}, + deploymentStrategy2: &appsv1.DeploymentStrategy{Type: appsv1.DeploymentStrategyType("foo")}, + }, + "DeploymentStrategy Conversion 2": { + deploymentStrategy1: &extensions.DeploymentStrategy{Type: extensions.DeploymentStrategyType("foo"), RollingUpdate: &extensionsRollingUpdate}, + deploymentStrategy2: &appsv1.DeploymentStrategy{Type: appsv1.DeploymentStrategyType("foo"), RollingUpdate: &appsv1RollingUpdate}, + }, + } + + for k, tc := range testcases { + // extensions -> appsv1 + internal1 := &appsv1.DeploymentStrategy{} + if err := legacyscheme.Scheme.Convert(tc.deploymentStrategy1, internal1, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", k, "extensions -> appsv1", err) + } + if !apiequality.Semantic.DeepEqual(internal1, tc.deploymentStrategy2) { + t.Errorf("%q - %q: expected\n\t%#v, got \n\t%#v", k, "extensions -> appsv1", tc.deploymentStrategy2, internal1) + } + + // appsv1 -> extensions + internal2 := &extensions.DeploymentStrategy{} + if err := legacyscheme.Scheme.Convert(tc.deploymentStrategy2, internal2, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", k, "appsv1 -> extensions", err) + } + if !apiequality.Semantic.DeepEqual(internal2, tc.deploymentStrategy1) { + t.Errorf("%q - %q: expected\n\t%#v, got \n\t%#v", k, "appsv1 -> extensions", tc.deploymentStrategy1, internal2) + } + } +} + +func TestV1RollingUpdateDeploymentConversion(t *testing.T) { + nilIntStr := intstr.IntOrString{} + maxUnavailable := intstr.FromInt(2) + maxSurge := intstr.FromInt(2) + testcases := map[string]struct { + rollingUpdateDeployment1 *extensions.RollingUpdateDeployment + rollingUpdateDeployment2 *appsv1.RollingUpdateDeployment + }{ + "RollingUpdateDeployment Conversion 1": { + rollingUpdateDeployment1: &extensions.RollingUpdateDeployment{}, + rollingUpdateDeployment2: &appsv1.RollingUpdateDeployment{MaxUnavailable: &nilIntStr, MaxSurge: &nilIntStr}, + }, + "RollingUpdateDeployment Conversion 2": { + rollingUpdateDeployment1: &extensions.RollingUpdateDeployment{MaxUnavailable: maxUnavailable}, + rollingUpdateDeployment2: &appsv1.RollingUpdateDeployment{MaxUnavailable: &maxUnavailable, MaxSurge: &nilIntStr}, + }, + "RollingUpdateDeployment Conversion 3": { + rollingUpdateDeployment1: &extensions.RollingUpdateDeployment{MaxSurge: maxSurge}, + rollingUpdateDeployment2: &appsv1.RollingUpdateDeployment{MaxSurge: &maxSurge, MaxUnavailable: &nilIntStr}, + }, + "RollingUpdateDeployment Conversion 4": { + rollingUpdateDeployment1: &extensions.RollingUpdateDeployment{MaxUnavailable: maxUnavailable, MaxSurge: maxSurge}, + rollingUpdateDeployment2: &appsv1.RollingUpdateDeployment{MaxUnavailable: &maxUnavailable, MaxSurge: &maxSurge}, + }, + } + + for k, tc := range testcases { + // extensions -> appsv1 + internal1 := &appsv1.RollingUpdateDeployment{} + if err := legacyscheme.Scheme.Convert(tc.rollingUpdateDeployment1, internal1, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", k, "extensions -> appsv1", err) + } + if !apiequality.Semantic.DeepEqual(internal1, tc.rollingUpdateDeployment2) { + t.Errorf("%q - %q: expected\n\t%#v, got \n\t%#v", k, "extensions -> appsv1", tc.rollingUpdateDeployment2, internal1) + } + + // appsv1 -> extensions + internal2 := &extensions.RollingUpdateDeployment{} + if err := legacyscheme.Scheme.Convert(tc.rollingUpdateDeployment2, internal2, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", k, "appsv1 -> extensions", err) + } + if !apiequality.Semantic.DeepEqual(internal2, tc.rollingUpdateDeployment1) { + t.Errorf("%q - %q: expected\n\t%#v, got \n\t%#v", k, "appsv1 -> extensions", tc.rollingUpdateDeployment1, internal2) + } + } +} + +func TestV1ReplicaSetSpecConversion(t *testing.T) { + replicas := new(int32) + *replicas = 2 + matchExpressions := []metav1.LabelSelectorRequirement{ + {Key: "foo", Operator: metav1.LabelSelectorOpIn, Values: []string{"foo"}}, + } + matchLabels := map[string]string{"foo": "bar"} + selector := &metav1.LabelSelector{MatchLabels: matchLabels, MatchExpressions: matchExpressions} + + testcases := map[string]struct { + replicaset1 *extensions.ReplicaSetSpec + replicaset2 *appsv1.ReplicaSetSpec + }{ + "ReplicaSetSpec Conversion 1": { + replicaset1: &extensions.ReplicaSetSpec{ + Replicas: *replicas, + MinReadySeconds: 2, + Template: api.PodTemplateSpec{ + Spec: api.PodSpec{ + SecurityContext: new(api.PodSecurityContext), + }, + }, + }, + replicaset2: &appsv1.ReplicaSetSpec{ + Replicas: replicas, + MinReadySeconds: 2, + Template: v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + SecurityContext: new(v1.PodSecurityContext), + }, + }, + }, + }, + "ReplicaSetSpec Conversion 2": { + replicaset1: &extensions.ReplicaSetSpec{ + Replicas: *replicas, + Selector: selector, + Template: api.PodTemplateSpec{ + Spec: api.PodSpec{ + SecurityContext: new(api.PodSecurityContext), + }, + }, + }, + replicaset2: &appsv1.ReplicaSetSpec{ + Replicas: replicas, + Selector: selector, + Template: v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + SecurityContext: new(v1.PodSecurityContext), + }, + }, + }, + }, + } + + for k, tc := range testcases { + // extensions -> appsv1 + internal1 := &appsv1.ReplicaSetSpec{} + if err := legacyscheme.Scheme.Convert(tc.replicaset1, internal1, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", k, "extensions -> appsv1", err) + } + + if !apiequality.Semantic.DeepEqual(internal1, tc.replicaset2) { + t.Errorf("%q - %q: expected\n\t%+v, got \n\t%+v", k, "extensions -> appsv1", tc.replicaset2, internal1) + } + + // appsv1 -> extensions + internal2 := &extensions.ReplicaSetSpec{} + if err := legacyscheme.Scheme.Convert(tc.replicaset2, internal2, nil); err != nil { + t.Errorf("%q - %q: unexpected error: %v", k, "appsv1 -> extensions", err) + } + if !apiequality.Semantic.DeepEqual(internal2, tc.replicaset1) { + t.Errorf("%q - %q: expected\n\t%+v, got \n\t%+v", k, "appsv1 -> extensions", tc.replicaset1, internal2) + } + } +} diff --git a/pkg/apis/apps/v1/defaults.go b/pkg/apis/apps/v1/defaults.go index 6803d038515..941a0c8e8d8 100644 --- a/pkg/apis/apps/v1/defaults.go +++ b/pkg/apis/apps/v1/defaults.go @@ -26,6 +26,49 @@ func addDefaultingFuncs(scheme *runtime.Scheme) error { return RegisterDefaults(scheme) } +// SetDefaults_Deployment sets additional defaults compared to its counterpart +// in extensions. These addons are: +// - MaxUnavailable during rolling update set to 25% (1 in extensions) +// - MaxSurge value during rolling update set to 25% (1 in extensions) +// - RevisionHistoryLimit set to 10 (not set in extensions) +// - ProgressDeadlineSeconds set to 600s (not set in extensions) +func SetDefaults_Deployment(obj *appsv1.Deployment) { + // Set DeploymentSpec.Replicas to 1 if it is not set. + if obj.Spec.Replicas == nil { + obj.Spec.Replicas = new(int32) + *obj.Spec.Replicas = 1 + } + strategy := &obj.Spec.Strategy + // Set default DeploymentStrategyType as RollingUpdate. + if strategy.Type == "" { + strategy.Type = appsv1.RollingUpdateDeploymentStrategyType + } + if strategy.Type == appsv1.RollingUpdateDeploymentStrategyType { + if strategy.RollingUpdate == nil { + rollingUpdate := appsv1.RollingUpdateDeployment{} + strategy.RollingUpdate = &rollingUpdate + } + if strategy.RollingUpdate.MaxUnavailable == nil { + // Set default MaxUnavailable as 25% by default. + maxUnavailable := intstr.FromString("25%") + strategy.RollingUpdate.MaxUnavailable = &maxUnavailable + } + if strategy.RollingUpdate.MaxSurge == nil { + // Set default MaxSurge as 25% by default. + maxSurge := intstr.FromString("25%") + strategy.RollingUpdate.MaxSurge = &maxSurge + } + } + if obj.Spec.RevisionHistoryLimit == nil { + obj.Spec.RevisionHistoryLimit = new(int32) + *obj.Spec.RevisionHistoryLimit = 10 + } + if obj.Spec.ProgressDeadlineSeconds == nil { + obj.Spec.ProgressDeadlineSeconds = new(int32) + *obj.Spec.ProgressDeadlineSeconds = 600 + } +} + func SetDefaults_DaemonSet(obj *appsv1.DaemonSet) { updateStrategy := &obj.Spec.UpdateStrategy if updateStrategy.Type == "" { @@ -47,3 +90,38 @@ func SetDefaults_DaemonSet(obj *appsv1.DaemonSet) { *obj.Spec.RevisionHistoryLimit = 10 } } + +func SetDefaults_StatefulSet(obj *appsv1.StatefulSet) { + if len(obj.Spec.PodManagementPolicy) == 0 { + obj.Spec.PodManagementPolicy = appsv1.OrderedReadyPodManagement + } + + if obj.Spec.UpdateStrategy.Type == "" { + obj.Spec.UpdateStrategy.Type = appsv1.RollingUpdateStatefulSetStrategyType + + // UpdateStrategy.RollingUpdate will take default values below. + obj.Spec.UpdateStrategy.RollingUpdate = &appsv1.RollingUpdateStatefulSetStrategy{} + } + + if obj.Spec.UpdateStrategy.Type == appsv1.RollingUpdateStatefulSetStrategyType && + obj.Spec.UpdateStrategy.RollingUpdate != nil && + obj.Spec.UpdateStrategy.RollingUpdate.Partition == nil { + obj.Spec.UpdateStrategy.RollingUpdate.Partition = new(int32) + *obj.Spec.UpdateStrategy.RollingUpdate.Partition = 0 + } + + if obj.Spec.Replicas == nil { + obj.Spec.Replicas = new(int32) + *obj.Spec.Replicas = 1 + } + if obj.Spec.RevisionHistoryLimit == nil { + obj.Spec.RevisionHistoryLimit = new(int32) + *obj.Spec.RevisionHistoryLimit = 10 + } +} +func SetDefaults_ReplicaSet(obj *appsv1.ReplicaSet) { + if obj.Spec.Replicas == nil { + obj.Spec.Replicas = new(int32) + *obj.Spec.Replicas = 1 + } +} diff --git a/pkg/apis/apps/v1/defaults_test.go b/pkg/apis/apps/v1/defaults_test.go index b4efdb1edfe..5dba88c7030 100644 --- a/pkg/apis/apps/v1/defaults_test.go +++ b/pkg/apis/apps/v1/defaults_test.go @@ -23,6 +23,7 @@ import ( appsv1 "k8s.io/api/apps/v1" "k8s.io/api/core/v1" apiequality "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" @@ -167,6 +168,382 @@ func TestSetDefaultDaemonSetSpec(t *testing.T) { } } +func TestSetDefaultStatefulSet(t *testing.T) { + defaultLabels := map[string]string{"foo": "bar"} + var defaultPartition int32 = 0 + var defaultReplicas int32 = 1 + + period := int64(v1.DefaultTerminationGracePeriodSeconds) + defaultTemplate := v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + DNSPolicy: v1.DNSClusterFirst, + RestartPolicy: v1.RestartPolicyAlways, + SecurityContext: &v1.PodSecurityContext{}, + TerminationGracePeriodSeconds: &period, + SchedulerName: api.DefaultSchedulerName, + }, + ObjectMeta: metav1.ObjectMeta{ + Labels: defaultLabels, + }, + } + + tests := []struct { + original *appsv1.StatefulSet + expected *appsv1.StatefulSet + }{ + { // labels and default update strategy + original: &appsv1.StatefulSet{ + Spec: appsv1.StatefulSetSpec{ + Template: defaultTemplate, + }, + }, + expected: &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Labels: defaultLabels, + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: &defaultReplicas, + Template: defaultTemplate, + PodManagementPolicy: appsv1.OrderedReadyPodManagement, + UpdateStrategy: appsv1.StatefulSetUpdateStrategy{ + Type: appsv1.RollingUpdateStatefulSetStrategyType, + RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{ + Partition: &defaultPartition, + }, + }, + RevisionHistoryLimit: newInt32(10), + }, + }, + }, + { // Alternate update strategy + original: &appsv1.StatefulSet{ + Spec: appsv1.StatefulSetSpec{ + Template: defaultTemplate, + UpdateStrategy: appsv1.StatefulSetUpdateStrategy{ + Type: appsv1.OnDeleteStatefulSetStrategyType, + }, + }, + }, + expected: &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Labels: defaultLabels, + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: &defaultReplicas, + Template: defaultTemplate, + PodManagementPolicy: appsv1.OrderedReadyPodManagement, + UpdateStrategy: appsv1.StatefulSetUpdateStrategy{ + Type: appsv1.OnDeleteStatefulSetStrategyType, + }, + RevisionHistoryLimit: newInt32(10), + }, + }, + }, + { // Parallel pod management policy. + original: &appsv1.StatefulSet{ + Spec: appsv1.StatefulSetSpec{ + Template: defaultTemplate, + PodManagementPolicy: appsv1.ParallelPodManagement, + }, + }, + expected: &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Labels: defaultLabels, + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: &defaultReplicas, + Template: defaultTemplate, + PodManagementPolicy: appsv1.ParallelPodManagement, + UpdateStrategy: appsv1.StatefulSetUpdateStrategy{ + Type: appsv1.RollingUpdateStatefulSetStrategyType, + RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{ + Partition: &defaultPartition, + }, + }, + RevisionHistoryLimit: newInt32(10), + }, + }, + }, + } + + for i, test := range tests { + original := test.original + expected := test.expected + obj2 := roundTrip(t, runtime.Object(original)) + got, ok := obj2.(*appsv1.StatefulSet) + if !ok { + t.Errorf("(%d) unexpected object: %v", i, got) + t.FailNow() + } + if !apiequality.Semantic.DeepEqual(got.Spec, expected.Spec) { + t.Errorf("(%d) got different than expected\ngot:\n\t%+v\nexpected:\n\t%+v", i, got.Spec, expected.Spec) + } + } +} + +func TestSetDefaultDeployment(t *testing.T) { + defaultIntOrString := intstr.FromString("25%") + differentIntOrString := intstr.FromInt(5) + period := int64(v1.DefaultTerminationGracePeriodSeconds) + defaultTemplate := v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + DNSPolicy: v1.DNSClusterFirst, + RestartPolicy: v1.RestartPolicyAlways, + SecurityContext: &v1.PodSecurityContext{}, + TerminationGracePeriodSeconds: &period, + SchedulerName: api.DefaultSchedulerName, + }, + } + tests := []struct { + original *appsv1.Deployment + expected *appsv1.Deployment + }{ + { + original: &appsv1.Deployment{}, + expected: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: newInt32(1), + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RollingUpdateDeploymentStrategyType, + RollingUpdate: &appsv1.RollingUpdateDeployment{ + MaxSurge: &defaultIntOrString, + MaxUnavailable: &defaultIntOrString, + }, + }, + RevisionHistoryLimit: newInt32(10), + ProgressDeadlineSeconds: newInt32(600), + Template: defaultTemplate, + }, + }, + }, + { + original: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: newInt32(5), + Strategy: appsv1.DeploymentStrategy{ + RollingUpdate: &appsv1.RollingUpdateDeployment{ + MaxSurge: &differentIntOrString, + }, + }, + }, + }, + expected: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: newInt32(5), + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RollingUpdateDeploymentStrategyType, + RollingUpdate: &appsv1.RollingUpdateDeployment{ + MaxSurge: &differentIntOrString, + MaxUnavailable: &defaultIntOrString, + }, + }, + RevisionHistoryLimit: newInt32(10), + ProgressDeadlineSeconds: newInt32(600), + Template: defaultTemplate, + }, + }, + }, + { + original: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: newInt32(3), + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RollingUpdateDeploymentStrategyType, + RollingUpdate: nil, + }, + }, + }, + expected: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: newInt32(3), + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RollingUpdateDeploymentStrategyType, + RollingUpdate: &appsv1.RollingUpdateDeployment{ + MaxSurge: &defaultIntOrString, + MaxUnavailable: &defaultIntOrString, + }, + }, + RevisionHistoryLimit: newInt32(10), + ProgressDeadlineSeconds: newInt32(600), + Template: defaultTemplate, + }, + }, + }, + { + original: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: newInt32(5), + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RecreateDeploymentStrategyType, + }, + RevisionHistoryLimit: newInt32(0), + }, + }, + expected: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: newInt32(5), + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RecreateDeploymentStrategyType, + }, + RevisionHistoryLimit: newInt32(0), + ProgressDeadlineSeconds: newInt32(600), + Template: defaultTemplate, + }, + }, + }, + { + original: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: newInt32(5), + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RecreateDeploymentStrategyType, + }, + ProgressDeadlineSeconds: newInt32(30), + RevisionHistoryLimit: newInt32(2), + }, + }, + expected: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Replicas: newInt32(5), + Strategy: appsv1.DeploymentStrategy{ + Type: appsv1.RecreateDeploymentStrategyType, + }, + ProgressDeadlineSeconds: newInt32(30), + RevisionHistoryLimit: newInt32(2), + Template: defaultTemplate, + }, + }, + }, + } + + for _, test := range tests { + original := test.original + expected := test.expected + obj2 := roundTrip(t, runtime.Object(original)) + got, ok := obj2.(*appsv1.Deployment) + if !ok { + t.Errorf("unexpected object: %v", got) + t.FailNow() + } + if !apiequality.Semantic.DeepEqual(got.Spec, expected.Spec) { + t.Errorf("object mismatch!\nexpected:\n\t%+v\ngot:\n\t%+v", got.Spec, expected.Spec) + } + } +} + +func TestDefaultDeploymentAvailability(t *testing.T) { + d := roundTrip(t, runtime.Object(&appsv1.Deployment{})).(*appsv1.Deployment) + + maxUnavailable, err := intstr.GetValueFromIntOrPercent(d.Spec.Strategy.RollingUpdate.MaxUnavailable, int(*(d.Spec.Replicas)), false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if *(d.Spec.Replicas)-int32(maxUnavailable) <= 0 { + t.Fatalf("the default value of maxUnavailable can lead to no active replicas during rolling update") + } +} + +func TestSetDefaultReplicaSetReplicas(t *testing.T) { + tests := []struct { + rs appsv1.ReplicaSet + expectReplicas int32 + }{ + { + rs: appsv1.ReplicaSet{ + Spec: appsv1.ReplicaSetSpec{ + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectReplicas: 1, + }, + { + rs: appsv1.ReplicaSet{ + Spec: appsv1.ReplicaSetSpec{ + Replicas: newInt32(0), + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectReplicas: 0, + }, + { + rs: appsv1.ReplicaSet{ + Spec: appsv1.ReplicaSetSpec{ + Replicas: newInt32(3), + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectReplicas: 3, + }, + } + + for _, test := range tests { + rs := &test.rs + obj2 := roundTrip(t, runtime.Object(rs)) + rs2, ok := obj2.(*appsv1.ReplicaSet) + if !ok { + t.Errorf("unexpected object: %v", rs2) + t.FailNow() + } + if rs2.Spec.Replicas == nil { + t.Errorf("unexpected nil Replicas") + } else if test.expectReplicas != *rs2.Spec.Replicas { + t.Errorf("expected: %d replicas, got: %d", test.expectReplicas, *rs2.Spec.Replicas) + } + } +} + +func TestDefaultRequestIsNotSetForReplicaSet(t *testing.T) { + s := v1.PodSpec{} + s.Containers = []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("100m"), + }, + }, + }, + } + rs := &appsv1.ReplicaSet{ + Spec: appsv1.ReplicaSetSpec{ + Replicas: newInt32(3), + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + Spec: s, + }, + }, + } + output := roundTrip(t, runtime.Object(rs)) + rs2 := output.(*appsv1.ReplicaSet) + defaultRequest := rs2.Spec.Template.Spec.Containers[0].Resources.Requests + requestValue := defaultRequest[v1.ResourceCPU] + if requestValue.String() != "0" { + t.Errorf("Expected 0 request value, got: %s", requestValue.String()) + } +} + func roundTrip(t *testing.T, obj runtime.Object) runtime.Object { data, err := runtime.Encode(legacyscheme.Codecs.LegacyCodec(SchemeGroupVersion), obj) if err != nil { diff --git a/pkg/registry/apps/rest/storage_apps.go b/pkg/registry/apps/rest/storage_apps.go index 0d06ef5032b..10b1c8a0bf5 100644 --- a/pkg/registry/apps/rest/storage_apps.go +++ b/pkg/registry/apps/rest/storage_apps.go @@ -118,11 +118,30 @@ func (p RESTStorageProvider) v1Storage(apiResourceConfigSource serverstorage.API version := appsapiv1.SchemeGroupVersion storage := map[string]rest.Storage{} + if apiResourceConfigSource.ResourceEnabled(version.WithResource("deployments")) { + deploymentStorage := deploymentstore.NewStorage(restOptionsGetter) + storage["deployments"] = deploymentStorage.Deployment + storage["deployments/status"] = deploymentStorage.Status + } + if apiResourceConfigSource.ResourceEnabled(version.WithResource("statefulsets")) { + statefulSetStorage := statefulsetstore.NewStorage(restOptionsGetter) + storage["statefulsets"] = statefulSetStorage.StatefulSet + storage["statefulsets/status"] = statefulSetStorage.Status + } if apiResourceConfigSource.ResourceEnabled(version.WithResource("daemonsets")) { daemonSetStorage, daemonSetStatusStorage := daemonsetstore.NewREST(restOptionsGetter) storage["daemonsets"] = daemonSetStorage storage["daemonsets/status"] = daemonSetStatusStorage } + if apiResourceConfigSource.ResourceEnabled(version.WithResource("replicasets")) { + replicaSetStorage := replicasetstore.NewStorage(restOptionsGetter) + storage["replicasets"] = replicaSetStorage.ReplicaSet + storage["replicasets/status"] = replicaSetStorage.Status + } + if apiResourceConfigSource.ResourceEnabled(version.WithResource("controllerrevisions")) { + historyStorage := controllerrevisionsstore.NewREST(restOptionsGetter) + storage["controllerrevisions"] = historyStorage + } return storage } diff --git a/pkg/registry/extensions/deployment/strategy.go b/pkg/registry/extensions/deployment/strategy.go index d7d3f755363..5cc8c79d6e4 100644 --- a/pkg/registry/extensions/deployment/strategy.go +++ b/pkg/registry/extensions/deployment/strategy.go @@ -19,6 +19,7 @@ package deployment import ( "fmt" + appsv1 "k8s.io/api/apps/v1" appsv1beta1 "k8s.io/api/apps/v1beta1" appsv1beta2 "k8s.io/api/apps/v1beta2" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" @@ -117,7 +118,7 @@ func (deploymentStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old // no-op for compatibility case extensionsv1beta1.SchemeGroupVersion: // no-op for compatibility - case appsv1beta2.SchemeGroupVersion: + case appsv1beta2.SchemeGroupVersion, appsv1.SchemeGroupVersion: // disallow mutation of selector allErrs = append(allErrs, apivalidation.ValidateImmutableField(newDeployment.Spec.Selector, oldDeployment.Spec.Selector, field.NewPath("spec").Child("selector"))...) default: diff --git a/pkg/registry/extensions/replicaset/strategy.go b/pkg/registry/extensions/replicaset/strategy.go index 5bb64ee52cf..10f71c138d1 100644 --- a/pkg/registry/extensions/replicaset/strategy.go +++ b/pkg/registry/extensions/replicaset/strategy.go @@ -22,6 +22,7 @@ import ( "fmt" "strconv" + appsv1 "k8s.io/api/apps/v1" appsv1beta2 "k8s.io/api/apps/v1beta2" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" apiequality "k8s.io/apimachinery/pkg/api/equality" @@ -127,7 +128,7 @@ func (rsStrategy) ValidateUpdate(ctx genericapirequest.Context, obj, old runtime switch groupVersion { case extensionsv1beta1.SchemeGroupVersion: // no-op for compatibility - case appsv1beta2.SchemeGroupVersion: + case appsv1beta2.SchemeGroupVersion, appsv1.SchemeGroupVersion: // disallow mutation of selector allErrs = append(allErrs, apivalidation.ValidateImmutableField(newReplicaSet.Spec.Selector, oldReplicaSet.Spec.Selector, field.NewPath("spec").Child("selector"))...) default: diff --git a/staging/src/k8s.io/api/apps/v1/register.go b/staging/src/k8s.io/api/apps/v1/register.go index a813d93cb0f..02710104674 100644 --- a/staging/src/k8s.io/api/apps/v1/register.go +++ b/staging/src/k8s.io/api/apps/v1/register.go @@ -44,8 +44,16 @@ var ( // Adds the list of known types to the given scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, + &Deployment{}, + &DeploymentList{}, + &StatefulSet{}, + &StatefulSetList{}, &DaemonSet{}, &DaemonSetList{}, + &ReplicaSet{}, + &ReplicaSetList{}, + &ControllerRevision{}, + &ControllerRevisionList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/staging/src/k8s.io/api/apps/v1/types.go b/staging/src/k8s.io/api/apps/v1/types.go index dc589e658de..3aa66553090 100644 --- a/staging/src/k8s.io/api/apps/v1/types.go +++ b/staging/src/k8s.io/api/apps/v1/types.go @@ -19,14 +19,415 @@ package v1 import ( "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" ) const ( ControllerRevisionHashLabelKey = "controller-revision-hash" + StatefulSetRevisionLabel = ControllerRevisionHashLabelKey + DeprecatedRollbackTo = "deprecated.deployment.rollback.to" DeprecatedTemplateGeneration = "deprecated.daemonset.template.generation" ) +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// StatefulSet represents a set of pods with consistent identities. +// Identities are defined as: +// - Network: A single stable DNS and hostname. +// - Storage: As many VolumeClaims as requested. +// The StatefulSet guarantees that a given network identity will always +// map to the same storage identity. +type StatefulSet struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Spec defines the desired identities of pods in this set. + // +optional + Spec StatefulSetSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + + // Status is the current status of Pods in this StatefulSet. This data + // may be out of date by some window of time. + // +optional + Status StatefulSetStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// PodManagementPolicyType defines the policy for creating pods under a stateful set. +type PodManagementPolicyType string + +const ( + // OrderedReadyPodManagement will create pods in strictly increasing order on + // scale up and strictly decreasing order on scale down, progressing only when + // the previous pod is ready or terminated. At most one pod will be changed + // at any time. + OrderedReadyPodManagement PodManagementPolicyType = "OrderedReady" + // ParallelPodManagement will create and delete pods as soon as the stateful set + // replica count is changed, and will not wait for pods to be ready or complete + // termination. + ParallelPodManagement = "Parallel" +) + +// StatefulSetUpdateStrategy indicates the strategy that the StatefulSet +// controller will use to perform updates. It includes any additional parameters +// necessary to perform the update for the indicated strategy. +type StatefulSetUpdateStrategy struct { + // Type indicates the type of the StatefulSetUpdateStrategy. + // Default is RollingUpdate. + // +optional + Type StatefulSetUpdateStrategyType `json:"type,omitempty" protobuf:"bytes,1,opt,name=type,casttype=StatefulSetStrategyType"` + // RollingUpdate is used to communicate parameters when Type is RollingUpdateStatefulSetStrategyType. + // +optional + RollingUpdate *RollingUpdateStatefulSetStrategy `json:"rollingUpdate,omitempty" protobuf:"bytes,2,opt,name=rollingUpdate"` +} + +// StatefulSetUpdateStrategyType is a string enumeration type that enumerates +// all possible update strategies for the StatefulSet controller. +type StatefulSetUpdateStrategyType string + +const ( + // RollingUpdateStatefulSetStrategyType indicates that update will be + // applied to all Pods in the StatefulSet with respect to the StatefulSet + // ordering constraints. When a scale operation is performed with this + // strategy, new Pods will be created from the specification version indicated + // by the StatefulSet's updateRevision. + RollingUpdateStatefulSetStrategyType = "RollingUpdate" + // OnDeleteStatefulSetStrategyType triggers the legacy behavior. Version + // tracking and ordered rolling restarts are disabled. Pods are recreated + // from the StatefulSetSpec when they are manually deleted. When a scale + // operation is performed with this strategy,specification version indicated + // by the StatefulSet's currentRevision. + OnDeleteStatefulSetStrategyType = "OnDelete" +) + +// RollingUpdateStatefulSetStrategy is used to communicate parameter for RollingUpdateStatefulSetStrategyType. +type RollingUpdateStatefulSetStrategy struct { + // Partition indicates the ordinal at which the StatefulSet should be + // partitioned. + // Default value is 0. + // +optional + Partition *int32 `json:"partition,omitempty" protobuf:"varint,1,opt,name=partition"` +} + +// A StatefulSetSpec is the specification of a StatefulSet. +type StatefulSetSpec struct { + // replicas is the desired number of replicas of the given Template. + // These are replicas in the sense that they are instantiations of the + // same Template, but individual replicas also have a consistent identity. + // If unspecified, defaults to 1. + // TODO: Consider a rename of this field. + // +optional + Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,1,opt,name=replicas"` + + // selector is a label query over pods that should match the replica count. + // If empty, defaulted to labels on the pod template. + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors + // +optional + Selector *metav1.LabelSelector `json:"selector,omitempty" protobuf:"bytes,2,opt,name=selector"` + + // template is the object that describes the pod that will be created if + // insufficient replicas are detected. Each pod stamped out by the StatefulSet + // will fulfill this Template, but have a unique identity from the rest + // of the StatefulSet. + Template v1.PodTemplateSpec `json:"template" protobuf:"bytes,3,opt,name=template"` + + // volumeClaimTemplates is a list of claims that pods are allowed to reference. + // The StatefulSet controller is responsible for mapping network identities to + // claims in a way that maintains the identity of a pod. Every claim in + // this list must have at least one matching (by name) volumeMount in one + // container in the template. A claim in this list takes precedence over + // any volumes in the template, with the same name. + // TODO: Define the behavior if a claim already exists with the same name. + // +optional + VolumeClaimTemplates []v1.PersistentVolumeClaim `json:"volumeClaimTemplates,omitempty" protobuf:"bytes,4,rep,name=volumeClaimTemplates"` + + // serviceName is the name of the service that governs this StatefulSet. + // This service must exist before the StatefulSet, and is responsible for + // the network identity of the set. Pods get DNS/hostnames that follow the + // pattern: pod-specific-string.serviceName.default.svc.cluster.local + // where "pod-specific-string" is managed by the StatefulSet controller. + ServiceName string `json:"serviceName" protobuf:"bytes,5,opt,name=serviceName"` + + // podManagementPolicy controls how pods are created during initial scale up, + // when replacing pods on nodes, or when scaling down. The default policy is + // `OrderedReady`, where pods are created in increasing order (pod-0, then + // pod-1, etc) and the controller will wait until each pod is ready before + // continuing. When scaling down, the pods are removed in the opposite order. + // The alternative policy is `Parallel` which will create pods in parallel + // to match the desired scale without waiting, and on scale down will delete + // all pods at once. + // +optional + PodManagementPolicy PodManagementPolicyType `json:"podManagementPolicy,omitempty" protobuf:"bytes,6,opt,name=podManagementPolicy,casttype=PodManagementPolicyType"` + + // updateStrategy indicates the StatefulSetUpdateStrategy that will be + // employed to update Pods in the StatefulSet when a revision is made to + // Template. + UpdateStrategy StatefulSetUpdateStrategy `json:"updateStrategy,omitempty" protobuf:"bytes,7,opt,name=updateStrategy"` + + // revisionHistoryLimit is the maximum number of revisions that will + // be maintained in the StatefulSet's revision history. The revision history + // consists of all revisions not represented by a currently applied + // StatefulSetSpec version. The default value is 10. + RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty" protobuf:"varint,8,opt,name=revisionHistoryLimit"` +} + +// StatefulSetStatus represents the current state of a StatefulSet. +type StatefulSetStatus struct { + // observedGeneration is the most recent generation observed for this StatefulSet. It corresponds to the + // StatefulSet's generation, which is updated on mutation by the API Server. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` + + // replicas is the number of Pods created by the StatefulSet controller. + Replicas int32 `json:"replicas" protobuf:"varint,2,opt,name=replicas"` + + // readyReplicas is the number of Pods created by the StatefulSet controller that have a Ready Condition. + ReadyReplicas int32 `json:"readyReplicas,omitempty" protobuf:"varint,3,opt,name=readyReplicas"` + + // currentReplicas is the number of Pods created by the StatefulSet controller from the StatefulSet version + // indicated by currentRevision. + CurrentReplicas int32 `json:"currentReplicas,omitempty" protobuf:"varint,4,opt,name=currentReplicas"` + + // updatedReplicas is the number of Pods created by the StatefulSet controller from the StatefulSet version + // indicated by updateRevision. + UpdatedReplicas int32 `json:"updatedReplicas,omitempty" protobuf:"varint,5,opt,name=updatedReplicas"` + + // currentRevision, if not empty, indicates the version of the StatefulSet used to generate Pods in the + // sequence [0,currentReplicas). + CurrentRevision string `json:"currentRevision,omitempty" protobuf:"bytes,6,opt,name=currentRevision"` + + // updateRevision, if not empty, indicates the version of the StatefulSet used to generate Pods in the sequence + // [replicas-updatedReplicas,replicas) + UpdateRevision string `json:"updateRevision,omitempty" protobuf:"bytes,7,opt,name=updateRevision"` + + // collisionCount is the count of hash collisions for the StatefulSet. The StatefulSet controller + // uses this field as a collision avoidance mechanism when it needs to create the name for the + // newest ControllerRevision. + // +optional + CollisionCount *int32 `json:"collisionCount,omitempty" protobuf:"varint,9,opt,name=collisionCount"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// StatefulSetList is a collection of StatefulSets. +type StatefulSetList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + Items []StatefulSet `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Deployment enables declarative updates for Pods and ReplicaSets. +type Deployment struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Specification of the desired behavior of the Deployment. + // +optional + Spec DeploymentSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + + // Most recently observed status of the Deployment. + // +optional + Status DeploymentStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// DeploymentSpec is the specification of the desired behavior of the Deployment. +type DeploymentSpec struct { + // Number of desired pods. This is a pointer to distinguish between explicit + // zero and not specified. Defaults to 1. + // +optional + Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,1,opt,name=replicas"` + + // Label selector for pods. Existing ReplicaSets whose pods are + // selected by this will be the ones affected by this deployment. + // +optional + Selector *metav1.LabelSelector `json:"selector,omitempty" protobuf:"bytes,2,opt,name=selector"` + + // Template describes the pods that will be created. + Template v1.PodTemplateSpec `json:"template" protobuf:"bytes,3,opt,name=template"` + + // The deployment strategy to use to replace existing pods with new ones. + // +optional + Strategy DeploymentStrategy `json:"strategy,omitempty" protobuf:"bytes,4,opt,name=strategy"` + + // Minimum number of seconds for which a newly created pod should be ready + // without any of its container crashing, for it to be considered available. + // Defaults to 0 (pod will be considered available as soon as it is ready) + // +optional + MinReadySeconds int32 `json:"minReadySeconds,omitempty" protobuf:"varint,5,opt,name=minReadySeconds"` + + // The number of old ReplicaSets to retain to allow rollback. + // This is a pointer to distinguish between explicit zero and not specified. + // Defaults to 10. + // +optional + RevisionHistoryLimit *int32 `json:"revisionHistoryLimit,omitempty" protobuf:"varint,6,opt,name=revisionHistoryLimit"` + + // Indicates that the deployment is paused. + // +optional + Paused bool `json:"paused,omitempty" protobuf:"varint,7,opt,name=paused"` + + // The maximum time in seconds for a deployment to make progress before it + // is considered to be failed. The deployment controller will continue to + // process failed deployments and a condition with a ProgressDeadlineExceeded + // reason will be surfaced in the deployment status. Note that progress will + // not be estimated during the time a deployment is paused. Defaults to 600s. + ProgressDeadlineSeconds *int32 `json:"progressDeadlineSeconds,omitempty" protobuf:"varint,9,opt,name=progressDeadlineSeconds"` +} + +const ( + // DefaultDeploymentUniqueLabelKey is the default key of the selector that is added + // to existing RCs (and label key that is added to its pods) to prevent the existing RCs + // to select new pods (and old pods being select by new RC). + DefaultDeploymentUniqueLabelKey string = "pod-template-hash" +) + +// DeploymentStrategy describes how to replace existing pods with new ones. +type DeploymentStrategy struct { + // Type of deployment. Can be "Recreate" or "RollingUpdate". Default is RollingUpdate. + // +optional + Type DeploymentStrategyType `json:"type,omitempty" protobuf:"bytes,1,opt,name=type,casttype=DeploymentStrategyType"` + + // Rolling update config params. Present only if DeploymentStrategyType = + // RollingUpdate. + //--- + // TODO: Update this to follow our convention for oneOf, whatever we decide it + // to be. + // +optional + RollingUpdate *RollingUpdateDeployment `json:"rollingUpdate,omitempty" protobuf:"bytes,2,opt,name=rollingUpdate"` +} + +type DeploymentStrategyType string + +const ( + // Kill all existing pods before creating new ones. + RecreateDeploymentStrategyType DeploymentStrategyType = "Recreate" + + // Replace the old RCs by new one using rolling update i.e gradually scale down the old RCs and scale up the new one. + RollingUpdateDeploymentStrategyType DeploymentStrategyType = "RollingUpdate" +) + +// Spec to control the desired behavior of rolling update. +type RollingUpdateDeployment struct { + // The maximum number of pods that can be unavailable during the update. + // Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + // Absolute number is calculated from percentage by rounding down. + // This can not be 0 if MaxSurge is 0. + // Defaults to 25%. + // Example: when this is set to 30%, the old RC can be scaled down to 70% of desired pods + // immediately when the rolling update starts. Once new pods are ready, old RC + // can be scaled down further, followed by scaling up the new RC, ensuring + // that the total number of pods available at all times during the update is at + // least 70% of desired pods. + // +optional + MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty" protobuf:"bytes,1,opt,name=maxUnavailable"` + + // The maximum number of pods that can be scheduled above the desired number of + // pods. + // Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). + // This can not be 0 if MaxUnavailable is 0. + // Absolute number is calculated from percentage by rounding up. + // Defaults to 25%. + // Example: when this is set to 30%, the new RC can be scaled up immediately when + // the rolling update starts, such that the total number of old and new pods do not exceed + // 130% of desired pods. Once old pods have been killed, + // new RC can be scaled up further, ensuring that total number of pods running + // at any time during the update is atmost 130% of desired pods. + // +optional + MaxSurge *intstr.IntOrString `json:"maxSurge,omitempty" protobuf:"bytes,2,opt,name=maxSurge"` +} + +// DeploymentStatus is the most recently observed status of the Deployment. +type DeploymentStatus struct { + // The generation observed by the deployment controller. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` + + // Total number of non-terminated pods targeted by this deployment (their labels match the selector). + // +optional + Replicas int32 `json:"replicas,omitempty" protobuf:"varint,2,opt,name=replicas"` + + // Total number of non-terminated pods targeted by this deployment that have the desired template spec. + // +optional + UpdatedReplicas int32 `json:"updatedReplicas,omitempty" protobuf:"varint,3,opt,name=updatedReplicas"` + + // Total number of ready pods targeted by this deployment. + // +optional + ReadyReplicas int32 `json:"readyReplicas,omitempty" protobuf:"varint,7,opt,name=readyReplicas"` + + // Total number of available pods (ready for at least minReadySeconds) targeted by this deployment. + // +optional + AvailableReplicas int32 `json:"availableReplicas,omitempty" protobuf:"varint,4,opt,name=availableReplicas"` + + // Total number of unavailable pods targeted by this deployment. This is the total number of + // pods that are still required for the deployment to have 100% available capacity. They may + // either be pods that are running but not yet available or pods that still have not been created. + // +optional + UnavailableReplicas int32 `json:"unavailableReplicas,omitempty" protobuf:"varint,5,opt,name=unavailableReplicas"` + + // Represents the latest available observations of a deployment's current state. + // +patchMergeKey=type + // +patchStrategy=merge + Conditions []DeploymentCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,6,rep,name=conditions"` + + // Count of hash collisions for the Deployment. The Deployment controller uses this + // field as a collision avoidance mechanism when it needs to create the name for the + // newest ReplicaSet. + // +optional + CollisionCount *int32 `json:"collisionCount,omitempty" protobuf:"varint,8,opt,name=collisionCount"` +} + +type DeploymentConditionType string + +// These are valid conditions of a deployment. +const ( + // Available means the deployment is available, ie. at least the minimum available + // replicas required are up and running for at least minReadySeconds. + DeploymentAvailable DeploymentConditionType = "Available" + // Progressing means the deployment is progressing. Progress for a deployment is + // considered when a new replica set is created or adopted, and when new pods scale + // up or old pods scale down. Progress is not estimated for paused deployments or + // when progressDeadlineSeconds is not specified. + DeploymentProgressing DeploymentConditionType = "Progressing" + // ReplicaFailure is added in a deployment when one of its pods fails to be created + // or deleted. + DeploymentReplicaFailure DeploymentConditionType = "ReplicaFailure" +) + +// DeploymentCondition describes the state of a deployment at a certain point. +type DeploymentCondition struct { + // Type of deployment condition. + Type DeploymentConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=DeploymentConditionType"` + // Status of the condition, one of True, False, Unknown. + Status v1.ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=k8s.io/api/core/v1.ConditionStatus"` + // The last time this condition was updated. + LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty" protobuf:"bytes,6,opt,name=lastUpdateTime"` + // Last time the condition transitioned from one status to another. + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,7,opt,name=lastTransitionTime"` + // The reason for the condition's last transition. + Reason string `json:"reason,omitempty" protobuf:"bytes,4,opt,name=reason"` + // A human readable message indicating details about the transition. + Message string `json:"message,omitempty" protobuf:"bytes,5,opt,name=message"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// DeploymentList is a list of Deployments. +type DeploymentList struct { + metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Items is the list of Deployments. + Items []Deployment `json:"items" protobuf:"bytes,2,rep,name=items"` +} + // DaemonSetUpdateStrategy is a struct used to control the update strategy for a DaemonSet. type DaemonSetUpdateStrategy struct { // Type of daemon set update. Can be "RollingUpdate" or "OnDelete". Default is RollingUpdate. @@ -199,3 +600,170 @@ type DaemonSetList struct { // A list of daemon sets. Items []DaemonSet `json:"items" protobuf:"bytes,2,rep,name=items"` } + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ReplicaSet ensures that a specified number of pod replicas are running at any given time. +type ReplicaSet struct { + metav1.TypeMeta `json:",inline"` + + // If the Labels of a ReplicaSet are empty, they are defaulted to + // be the same as the Pod(s) that the ReplicaSet manages. + // Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Spec defines the specification of the desired behavior of the ReplicaSet. + // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status + // +optional + Spec ReplicaSetSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + + // Status is the most recently observed status of the ReplicaSet. + // This data may be out of date by some window of time. + // Populated by the system. + // Read-only. + // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#spec-and-status + // +optional + Status ReplicaSetStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ReplicaSetList is a collection of ReplicaSets. +type ReplicaSetList struct { + metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // List of ReplicaSets. + // More info: https://kubernetes.io/docs/concepts/workloads/controllers/replicationcontroller + Items []ReplicaSet `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// ReplicaSetSpec is the specification of a ReplicaSet. +type ReplicaSetSpec struct { + // Replicas is the number of desired replicas. + // This is a pointer to distinguish between explicit zero and unspecified. + // Defaults to 1. + // More info: https://kubernetes.io/docs/concepts/workloads/controllers/replicationcontroller/#what-is-a-replicationcontroller + // +optional + Replicas *int32 `json:"replicas,omitempty" protobuf:"varint,1,opt,name=replicas"` + + // Minimum number of seconds for which a newly created pod should be ready + // without any of its container crashing, for it to be considered available. + // Defaults to 0 (pod will be considered available as soon as it is ready) + // +optional + MinReadySeconds int32 `json:"minReadySeconds,omitempty" protobuf:"varint,4,opt,name=minReadySeconds"` + + // Selector is a label query over pods that should match the replica count. + // If the selector is empty, it is defaulted to the labels present on the pod template. + // Label keys and values that must match in order to be controlled by this replica set. + // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors + // +optional + Selector *metav1.LabelSelector `json:"selector,omitempty" protobuf:"bytes,2,opt,name=selector"` + + // Template is the object that describes the pod that will be created if + // insufficient replicas are detected. + // More info: https://kubernetes.io/docs/concepts/workloads/controllers/replicationcontroller#pod-template + // +optional + Template v1.PodTemplateSpec `json:"template,omitempty" protobuf:"bytes,3,opt,name=template"` +} + +// ReplicaSetStatus represents the current status of a ReplicaSet. +type ReplicaSetStatus struct { + // Replicas is the most recently oberved number of replicas. + // More info: https://kubernetes.io/docs/concepts/workloads/controllers/replicationcontroller/#what-is-a-replicationcontroller + Replicas int32 `json:"replicas" protobuf:"varint,1,opt,name=replicas"` + + // The number of pods that have labels matching the labels of the pod template of the replicaset. + // +optional + FullyLabeledReplicas int32 `json:"fullyLabeledReplicas,omitempty" protobuf:"varint,2,opt,name=fullyLabeledReplicas"` + + // The number of ready replicas for this replica set. + // +optional + ReadyReplicas int32 `json:"readyReplicas,omitempty" protobuf:"varint,4,opt,name=readyReplicas"` + + // The number of available replicas (ready for at least minReadySeconds) for this replica set. + // +optional + AvailableReplicas int32 `json:"availableReplicas,omitempty" protobuf:"varint,5,opt,name=availableReplicas"` + + // ObservedGeneration reflects the generation of the most recently observed ReplicaSet. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,3,opt,name=observedGeneration"` + + // Represents the latest available observations of a replica set's current state. + // +optional + // +patchMergeKey=type + // +patchStrategy=merge + Conditions []ReplicaSetCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,6,rep,name=conditions"` +} + +type ReplicaSetConditionType string + +// These are valid conditions of a replica set. +const ( + // ReplicaSetReplicaFailure is added in a replica set when one of its pods fails to be created + // due to insufficient quota, limit ranges, pod security policy, node selectors, etc. or deleted + // due to kubelet being down or finalizers are failing. + ReplicaSetReplicaFailure ReplicaSetConditionType = "ReplicaFailure" +) + +// ReplicaSetCondition describes the state of a replica set at a certain point. +type ReplicaSetCondition struct { + // Type of replica set condition. + Type ReplicaSetConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=ReplicaSetConditionType"` + // Status of the condition, one of True, False, Unknown. + Status v1.ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=k8s.io/api/core/v1.ConditionStatus"` + // The last time the condition transitioned from one status to another. + // +optional + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty" protobuf:"bytes,3,opt,name=lastTransitionTime"` + // The reason for the condition's last transition. + // +optional + Reason string `json:"reason,omitempty" protobuf:"bytes,4,opt,name=reason"` + // A human readable message indicating details about the transition. + // +optional + Message string `json:"message,omitempty" protobuf:"bytes,5,opt,name=message"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ControllerRevision implements an immutable snapshot of state data. Clients +// are responsible for serializing and deserializing the objects that contain +// their internal state. +// Once a ControllerRevision has been successfully created, it can not be updated. +// The API Server will fail validation of all requests that attempt to mutate +// the Data field. ControllerRevisions may, however, be deleted. Note that, due to its use by both +// the DaemonSet and StatefulSet controllers for update and rollback, this object is beta. However, +// it may be subject to name and representation changes in future releases, and clients should not +// depend on its stability. It is primarily for internal use by controllers. +type ControllerRevision struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Data is the serialized representation of the state. + Data runtime.RawExtension `json:"data,omitempty" protobuf:"bytes,2,opt,name=data"` + + // Revision indicates the revision of the state represented by Data. + Revision int64 `json:"revision" protobuf:"varint,3,opt,name=revision"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ControllerRevisionList is a resource containing a list of ControllerRevision objects. +type ControllerRevisionList struct { + metav1.TypeMeta `json:",inline"` + + // More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Items is the list of ControllerRevisions + Items []ControllerRevision `json:"items" protobuf:"bytes,2,rep,name=items"` +} diff --git a/test/integration/etcd/etcd_storage_path_test.go b/test/integration/etcd/etcd_storage_path_test.go index 2f0235f09b4..217e8eb2536 100644 --- a/test/integration/etcd/etcd_storage_path_test.go +++ b/test/integration/etcd/etcd_storage_path_test.go @@ -181,6 +181,26 @@ var etcdStorageData = map[schema.GroupVersionResource]struct { expectedEtcdPath: "/registry/daemonsets/etcdstoragepathtestnamespace/ds6", expectedGVK: gvkP("extensions", "v1beta1", "DaemonSet"), }, + gvr("apps", "v1", "deployments"): { + stub: `{"metadata": {"name": "deployment4"}, "spec": {"selector": {"matchLabels": {"f": "z"}}, "template": {"metadata": {"labels": {"f": "z"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container6"}]}}}}`, + expectedEtcdPath: "/registry/deployments/etcdstoragepathtestnamespace/deployment4", + expectedGVK: gvkP("extensions", "v1beta1", "Deployment"), + }, + gvr("apps", "v1", "statefulsets"): { + stub: `{"metadata": {"name": "ss3"}, "spec": {"selector": {"matchLabels": {"a": "b"}}, "template": {"metadata": {"labels": {"a": "b"}}}}}`, + expectedEtcdPath: "/registry/statefulsets/etcdstoragepathtestnamespace/ss3", + expectedGVK: gvkP("apps", "v1beta1", "StatefulSet"), + }, + gvr("apps", "v1", "replicasets"): { + stub: `{"metadata": {"name": "rs3"}, "spec": {"selector": {"matchLabels": {"g": "h"}}, "template": {"metadata": {"labels": {"g": "h"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container4"}]}}}}`, + expectedEtcdPath: "/registry/replicasets/etcdstoragepathtestnamespace/rs3", + expectedGVK: gvkP("extensions", "v1beta1", "ReplicaSet"), + }, + gvr("apps", "v1", "controllerrevisions"): { + stub: `{"metadata":{"name":"crs3"},"data":{"name":"abc","namespace":"default","creationTimestamp":null,"Spec":{"Replicas":0,"Selector":{"matchLabels":{"foo":"bar"}},"Template":{"creationTimestamp":null,"labels":{"foo":"bar"},"Spec":{"Volumes":null,"InitContainers":null,"Containers":null,"RestartPolicy":"Always","TerminationGracePeriodSeconds":null,"ActiveDeadlineSeconds":null,"DNSPolicy":"ClusterFirst","NodeSelector":null,"ServiceAccountName":"","AutomountServiceAccountToken":null,"NodeName":"","SecurityContext":null,"ImagePullSecrets":null,"Hostname":"","Subdomain":"","Affinity":null,"SchedulerName":"","Tolerations":null,"HostAliases":null}},"VolumeClaimTemplates":null,"ServiceName":""},"Status":{"ObservedGeneration":null,"Replicas":0}},"revision":0}`, + expectedEtcdPath: "/registry/controllerrevisions/etcdstoragepathtestnamespace/crs3", + expectedGVK: gvkP("apps", "v1beta1", "ControllerRevision"), + }, // -- // k8s.io/kubernetes/pkg/apis/autoscaling/v1