diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index f2e036e84c..eb2aabf7e4 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -49,7 +49,9 @@ import ( serverstorage "k8s.io/apiserver/pkg/server/storage" "k8s.io/kubernetes/cmd/kube-apiserver/app/options" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/batch" + "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/capabilities" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" informers "k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion" @@ -243,6 +245,8 @@ func BuildMasterConfig(s *options.ServerRunOptions) (*master.Config, informers.S if err != nil { return nil, nil, fmt.Errorf("error in initializing storage factory: %s", err) } + // keep Deployments in extensions for backwards compatibility, we'll have to migrate at some point, eventually + storageFactory.AddCohabitatingResources(extensions.Resource("deployments"), apps.Resource("deployments")) for _, override := range s.Etcd.EtcdServersOverrides { tokens := strings.Split(override, "#") if len(tokens) != 2 { diff --git a/hack/make-rules/test-cmd-util.sh b/hack/make-rules/test-cmd-util.sh index 5dbf12a8db..e1b8c78ab6 100644 --- a/hack/make-rules/test-cmd-util.sh +++ b/hack/make-rules/test-cmd-util.sh @@ -1133,7 +1133,7 @@ run_kubectl_get_tests() { kube::test::if_has_string "${output_message}" "/apis/apps/v1beta1/namespaces/default/statefulsets 200 OK" kube::test::if_has_string "${output_message}" "/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers 200" kube::test::if_has_string "${output_message}" "/apis/batch/v1/namespaces/default/jobs 200 OK" - kube::test::if_has_string "${output_message}" "/apis/extensions/v1beta1/namespaces/default/deployments 200 OK" + kube::test::if_has_string "${output_message}" "/apis/apps/v1beta1/namespaces/default/deployments 200 OK" kube::test::if_has_string "${output_message}" "/apis/extensions/v1beta1/namespaces/default/replicasets 200 OK" ### Test --allow-missing-template-keys @@ -2270,6 +2270,11 @@ run_deployment_tests() { kubectl create deployment test-nginx --image=gcr.io/google-containers/nginx:test-cmd # Post-Condition: Deployment has 2 replicas defined in its spec. kube::test::get_object_assert 'deploy test-nginx' "{{$container_name_field}}" 'nginx' + # Ensure we can interact with deployments through extensions and apps endpoints + output_message=$(kubectl get deployment.extensions -o=jsonpath='{.items[0].apiVersion}' 2>&1 "${kube_flags[@]}") + kube::test::if_has_string "${output_message}" 'extensions/v1beta1' + output_message=$(kubectl get deployment.apps -o=jsonpath='{.items[0].apiVersion}' 2>&1 "${kube_flags[@]}") + kube::test::if_has_string "${output_message}" 'apps/v1beta1' # Clean up kubectl delete deployment test-nginx "${kube_flags[@]}" @@ -2882,7 +2887,7 @@ runTests() { kube::test::get_object_assert rolebinding/sarole "{{range.subjects}}{{.namespace}}:{{end}}" 'otherns:' kube::test::get_object_assert rolebinding/sarole "{{range.subjects}}{{.name}}:{{end}}" 'sa-name:' fi - + if kube::test::if_supports_resource "${roles}" ; then kubectl create "${kube_flags[@]}" role pod-admin --verb=* --resource=pods kube::test::get_object_assert role/pod-admin "{{range.rules}}{{range.verbs}}{{.}}:{{end}}{{end}}" '\*:' diff --git a/pkg/api/defaulting_test.go b/pkg/api/defaulting_test.go index b3f9e5d95b..b145548bf0 100644 --- a/pkg/api/defaulting_test.go +++ b/pkg/api/defaulting_test.go @@ -105,6 +105,8 @@ func TestDefaulting(t *testing.T) { {Group: "extensions", Version: "v1beta1", Kind: "DaemonSetList"}: {}, {Group: "extensions", Version: "v1beta1", Kind: "Deployment"}: {}, {Group: "extensions", Version: "v1beta1", Kind: "DeploymentList"}: {}, + {Group: "apps", Version: "v1beta1", Kind: "Deployment"}: {}, + {Group: "apps", Version: "v1beta1", Kind: "DeploymentList"}: {}, {Group: "extensions", Version: "v1beta1", Kind: "ReplicaSet"}: {}, {Group: "extensions", Version: "v1beta1", Kind: "ReplicaSetList"}: {}, {Group: "rbac.authorization.k8s.io", Version: "v1alpha1", Kind: "ClusterRoleBinding"}: {}, diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index 58429422d0..a259a5eb6e 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -474,6 +474,13 @@ func coreFuncs(t apitesting.TestingCommon) []interface{} { func extensionFuncs(t apitesting.TestingCommon) []interface{} { return []interface{}{ + func(j *extensions.DeploymentSpec, c fuzz.Continue) { + c.FuzzNoCustom(j) // fuzz self without calling this function again + rhl := int32(c.Rand.Int31()) + pds := int32(c.Rand.Int31()) + j.RevisionHistoryLimit = &rhl + j.ProgressDeadlineSeconds = &pds + }, func(j *extensions.DeploymentStrategy, c fuzz.Continue) { c.FuzzNoCustom(j) // fuzz self without calling this function again // Ensure that strategyType is one of valid values. diff --git a/pkg/apis/apps/register.go b/pkg/apis/apps/register.go index 639c1be48f..dae876f37b 100644 --- a/pkg/apis/apps/register.go +++ b/pkg/apis/apps/register.go @@ -19,6 +19,7 @@ package apps import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubernetes/pkg/apis/extensions" ) var ( @@ -46,6 +47,10 @@ func Resource(resource string) schema.GroupResource { func addKnownTypes(scheme *runtime.Scheme) error { // TODO this will get cleaned up with the scheme types are fixed scheme.AddKnownTypes(SchemeGroupVersion, + &extensions.Deployment{}, + &extensions.DeploymentList{}, + &extensions.DeploymentRollback{}, + &extensions.Scale{}, &StatefulSet{}, &StatefulSetList{}, ) diff --git a/pkg/apis/apps/v1beta1/conversion.go b/pkg/apis/apps/v1beta1/conversion.go index 90ceddd359..6735517fae 100644 --- a/pkg/apis/apps/v1beta1/conversion.go +++ b/pkg/apis/apps/v1beta1/conversion.go @@ -22,9 +22,11 @@ import ( 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" - v1 "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/apis/apps" + "k8s.io/kubernetes/pkg/apis/extensions" ) func addConversionFuncs(scheme *runtime.Scheme) error { @@ -35,21 +37,49 @@ func addConversionFuncs(scheme *runtime.Scheme) error { err := scheme.AddConversionFuncs( Convert_v1beta1_StatefulSetSpec_To_apps_StatefulSetSpec, Convert_apps_StatefulSetSpec_To_v1beta1_StatefulSetSpec, + // extensions + // TODO: below conversions should be dropped in favor of auto-generated + // ones, see https://github.com/kubernetes/kubernetextensionsssues/39865 + Convert_v1beta1_ScaleStatus_To_extensions_ScaleStatus, + Convert_extensions_ScaleStatus_To_v1beta1_ScaleStatus, + Convert_v1beta1_DeploymentSpec_To_extensions_DeploymentSpec, + Convert_extensions_DeploymentSpec_To_v1beta1_DeploymentSpec, + Convert_v1beta1_DeploymentStrategy_To_extensions_DeploymentStrategy, + Convert_extensions_DeploymentStrategy_To_v1beta1_DeploymentStrategy, + Convert_v1beta1_RollingUpdateDeployment_To_extensions_RollingUpdateDeployment, + Convert_extensions_RollingUpdateDeployment_To_v1beta1_RollingUpdateDeployment, ) if err != nil { return err } - return scheme.AddFieldLabelConversionFunc("apps/v1beta1", "StatefulSet", + // Add field label conversions for kinds having selectable nothing but ObjectMeta fields. + err = scheme.AddFieldLabelConversionFunc("apps/v1beta1", "StatefulSet", func(label, value string) (string, string, error) { switch label { case "metadata.name", "metadata.namespace", "status.successful": return label, value, nil default: - return "", "", fmt.Errorf("field label not supported: %s", label) + return "", "", fmt.Errorf("field label not supported for StatefulSet: %s", label) } - }, - ) + }) + if err != nil { + return err + } + err = api.Scheme.AddFieldLabelConversionFunc("apps/v1beta1", "Deployment", + func(label, value string) (string, string, error) { + switch label { + case "metadata.name", "metadata.namespace": + return label, value, nil + default: + return "", "", fmt.Errorf("field label %q not supported for Deployment", label) + } + }) + if err != nil { + return err + } + + return nil } func Convert_v1beta1_StatefulSetSpec_To_apps_StatefulSetSpec(in *StatefulSetSpec, out *apps.StatefulSetSpec, s conversion.Scope) error { @@ -112,3 +142,156 @@ func Convert_apps_StatefulSetSpec_To_v1beta1_StatefulSetSpec(in *apps.StatefulSe out.ServiceName = in.ServiceName return nil } + +func Convert_extensions_ScaleStatus_To_v1beta1_ScaleStatus(in *extensions.ScaleStatus, out *ScaleStatus, s conversion.Scope) error { + out.Replicas = int32(in.Replicas) + + out.Selector = nil + out.TargetSelector = "" + if in.Selector != nil { + if in.Selector.MatchExpressions == nil || len(in.Selector.MatchExpressions) == 0 { + out.Selector = in.Selector.MatchLabels + } + + selector, err := metav1.LabelSelectorAsSelector(in.Selector) + if err != nil { + return fmt.Errorf("invalid label selector: %v", err) + } + out.TargetSelector = selector.String() + } + return nil +} + +func Convert_v1beta1_ScaleStatus_To_extensions_ScaleStatus(in *ScaleStatus, out *extensions.ScaleStatus, s conversion.Scope) error { + out.Replicas = in.Replicas + + // Normally when 2 fields map to the same internal value we favor the old field, since + // old clients can't be expected to know about new fields but clients that know about the + // new field can be expected to know about the old field (though that's not quite true, due + // to kubectl apply). However, these fields are readonly, so any non-nil value should work. + if in.TargetSelector != "" { + labelSelector, err := metav1.ParseToLabelSelector(in.TargetSelector) + if err != nil { + out.Selector = nil + return fmt.Errorf("failed to parse target selector: %v", err) + } + out.Selector = labelSelector + } else if in.Selector != nil { + out.Selector = new(metav1.LabelSelector) + selector := make(map[string]string) + for key, val := range in.Selector { + selector[key] = val + } + out.Selector.MatchLabels = selector + } else { + out.Selector = nil + } + return nil +} + +func Convert_v1beta1_DeploymentSpec_To_extensions_DeploymentSpec(in *DeploymentSpec, out *extensions.DeploymentSpec, s conversion.Scope) error { + if in.Replicas != nil { + out.Replicas = *in.Replicas + } + out.Selector = in.Selector + if err := v1.Convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { + return err + } + if err := Convert_v1beta1_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.RollbackTo != nil { + out.RollbackTo = new(extensions.RollbackConfig) + out.RollbackTo.Revision = in.RollbackTo.Revision + } else { + out.RollbackTo = nil + } + if in.ProgressDeadlineSeconds != nil { + out.ProgressDeadlineSeconds = new(int32) + *out.ProgressDeadlineSeconds = *in.ProgressDeadlineSeconds + } + return nil +} + +func Convert_extensions_DeploymentSpec_To_v1beta1_DeploymentSpec(in *extensions.DeploymentSpec, out *DeploymentSpec, s conversion.Scope) error { + out.Replicas = &in.Replicas + out.Selector = in.Selector + if err := v1.Convert_api_PodTemplateSpec_To_v1_PodTemplateSpec(&in.Template, &out.Template, s); err != nil { + return err + } + if err := Convert_extensions_DeploymentStrategy_To_v1beta1_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.RollbackTo != nil { + out.RollbackTo = new(RollbackConfig) + out.RollbackTo.Revision = int64(in.RollbackTo.Revision) + } else { + out.RollbackTo = nil + } + if in.ProgressDeadlineSeconds != nil { + out.ProgressDeadlineSeconds = new(int32) + *out.ProgressDeadlineSeconds = *in.ProgressDeadlineSeconds + } + return nil +} + +func Convert_extensions_DeploymentStrategy_To_v1beta1_DeploymentStrategy(in *extensions.DeploymentStrategy, out *DeploymentStrategy, s conversion.Scope) error { + out.Type = DeploymentStrategyType(in.Type) + if in.RollingUpdate != nil { + out.RollingUpdate = new(RollingUpdateDeployment) + if err := Convert_extensions_RollingUpdateDeployment_To_v1beta1_RollingUpdateDeployment(in.RollingUpdate, out.RollingUpdate, s); err != nil { + return err + } + } else { + out.RollingUpdate = nil + } + return nil +} + +func Convert_v1beta1_DeploymentStrategy_To_extensions_DeploymentStrategy(in *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_v1beta1_RollingUpdateDeployment_To_extensions_RollingUpdateDeployment(in.RollingUpdate, out.RollingUpdate, s); err != nil { + return err + } + } else { + out.RollingUpdate = nil + } + return nil +} + +func Convert_v1beta1_RollingUpdateDeployment_To_extensions_RollingUpdateDeployment(in *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_v1beta1_RollingUpdateDeployment(in *extensions.RollingUpdateDeployment, out *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 +} diff --git a/pkg/apis/apps/v1beta1/defaults.go b/pkg/apis/apps/v1beta1/defaults.go index bdab631e02..004cecd3f0 100644 --- a/pkg/apis/apps/v1beta1/defaults.go +++ b/pkg/apis/apps/v1beta1/defaults.go @@ -19,12 +19,14 @@ package v1beta1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" ) func addDefaultingFuncs(scheme *runtime.Scheme) error { RegisterDefaults(scheme) return scheme.AddDefaultingFuncs( SetDefaults_StatefulSet, + SetDefaults_Deployment, ) } @@ -45,3 +47,57 @@ func SetDefaults_StatefulSet(obj *StatefulSet) { *obj.Spec.Replicas = 1 } } + +// 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 2 (not set in extensions) +// - ProgressDeadlineSeconds set to 600s (not set in extensions) +func SetDefaults_Deployment(obj *Deployment) { + // Default labels and selector to labels from pod template spec. + labels := obj.Spec.Template.Labels + + if labels != nil { + if obj.Spec.Selector == nil { + obj.Spec.Selector = &metav1.LabelSelector{MatchLabels: labels} + } + if len(obj.Labels) == 0 { + obj.Labels = labels + } + } + // 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 = RollingUpdateDeploymentStrategyType + } + if strategy.Type == RollingUpdateDeploymentStrategyType { + if strategy.RollingUpdate == nil { + rollingUpdate := 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 = 2 + } + if obj.Spec.ProgressDeadlineSeconds == nil { + obj.Spec.ProgressDeadlineSeconds = new(int32) + *obj.Spec.ProgressDeadlineSeconds = 600 + } +} diff --git a/pkg/apis/apps/v1beta1/defaults_test.go b/pkg/apis/apps/v1beta1/defaults_test.go new file mode 100644 index 0000000000..4ddb7e0a39 --- /dev/null +++ b/pkg/apis/apps/v1beta1/defaults_test.go @@ -0,0 +1,219 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1_test + +import ( + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/kubernetes/pkg/api" + _ "k8s.io/kubernetes/pkg/api/install" + "k8s.io/kubernetes/pkg/api/v1" + _ "k8s.io/kubernetes/pkg/apis/apps/install" + . "k8s.io/kubernetes/pkg/apis/apps/v1beta1" +) + +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 *Deployment + expected *Deployment + }{ + { + original: &Deployment{}, + expected: &Deployment{ + Spec: DeploymentSpec{ + Replicas: newInt32(1), + Strategy: DeploymentStrategy{ + Type: RollingUpdateDeploymentStrategyType, + RollingUpdate: &RollingUpdateDeployment{ + MaxSurge: &defaultIntOrString, + MaxUnavailable: &defaultIntOrString, + }, + }, + RevisionHistoryLimit: newInt32(2), + ProgressDeadlineSeconds: newInt32(600), + Template: defaultTemplate, + }, + }, + }, + { + original: &Deployment{ + Spec: DeploymentSpec{ + Replicas: newInt32(5), + Strategy: DeploymentStrategy{ + RollingUpdate: &RollingUpdateDeployment{ + MaxSurge: &differentIntOrString, + }, + }, + }, + }, + expected: &Deployment{ + Spec: DeploymentSpec{ + Replicas: newInt32(5), + Strategy: DeploymentStrategy{ + Type: RollingUpdateDeploymentStrategyType, + RollingUpdate: &RollingUpdateDeployment{ + MaxSurge: &differentIntOrString, + MaxUnavailable: &defaultIntOrString, + }, + }, + RevisionHistoryLimit: newInt32(2), + ProgressDeadlineSeconds: newInt32(600), + Template: defaultTemplate, + }, + }, + }, + { + original: &Deployment{ + Spec: DeploymentSpec{ + Replicas: newInt32(3), + Strategy: DeploymentStrategy{ + Type: RollingUpdateDeploymentStrategyType, + RollingUpdate: nil, + }, + }, + }, + expected: &Deployment{ + Spec: DeploymentSpec{ + Replicas: newInt32(3), + Strategy: DeploymentStrategy{ + Type: RollingUpdateDeploymentStrategyType, + RollingUpdate: &RollingUpdateDeployment{ + MaxSurge: &defaultIntOrString, + MaxUnavailable: &defaultIntOrString, + }, + }, + RevisionHistoryLimit: newInt32(2), + ProgressDeadlineSeconds: newInt32(600), + Template: defaultTemplate, + }, + }, + }, + { + original: &Deployment{ + Spec: DeploymentSpec{ + Replicas: newInt32(5), + Strategy: DeploymentStrategy{ + Type: RecreateDeploymentStrategyType, + }, + RevisionHistoryLimit: newInt32(0), + }, + }, + expected: &Deployment{ + Spec: DeploymentSpec{ + Replicas: newInt32(5), + Strategy: DeploymentStrategy{ + Type: RecreateDeploymentStrategyType, + }, + RevisionHistoryLimit: newInt32(0), + ProgressDeadlineSeconds: newInt32(600), + Template: defaultTemplate, + }, + }, + }, + { + original: &Deployment{ + Spec: DeploymentSpec{ + Replicas: newInt32(5), + Strategy: DeploymentStrategy{ + Type: RecreateDeploymentStrategyType, + }, + ProgressDeadlineSeconds: newInt32(30), + RevisionHistoryLimit: newInt32(2), + }, + }, + expected: &Deployment{ + Spec: DeploymentSpec{ + Replicas: newInt32(5), + Strategy: DeploymentStrategy{ + Type: 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.(*Deployment) + if !ok { + t.Errorf("unexpected object: %v", got) + t.FailNow() + } + if !reflect.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(&Deployment{})).(*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 roundTrip(t *testing.T, obj runtime.Object) runtime.Object { + data, err := runtime.Encode(api.Codecs.LegacyCodec(SchemeGroupVersion), obj) + if err != nil { + t.Errorf("%v\n %#v", err, obj) + return nil + } + obj2, err := runtime.Decode(api.Codecs.UniversalDecoder(), data) + if err != nil { + t.Errorf("%v\nData: %s\nSource: %#v", err, string(data), obj) + return nil + } + obj3 := reflect.New(reflect.TypeOf(obj).Elem()).Interface().(runtime.Object) + err = api.Scheme.Convert(obj2, obj3, nil) + if err != nil { + t.Errorf("%v\nSource: %#v", err, obj2) + return nil + } + return obj3 +} + +func newInt32(val int32) *int32 { + p := new(int32) + *p = val + return p +} diff --git a/pkg/apis/apps/v1beta1/register.go b/pkg/apis/apps/v1beta1/register.go index 4e7829d94b..6e618e1d8f 100644 --- a/pkg/apis/apps/v1beta1/register.go +++ b/pkg/apis/apps/v1beta1/register.go @@ -41,12 +41,13 @@ var ( // Adds the list of known types to api.Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, + &Deployment{}, + &DeploymentList{}, + &DeploymentRollback{}, + &Scale{}, &StatefulSet{}, &StatefulSetList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil } - -func (obj *StatefulSet) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta } -func (obj *StatefulSetList) GetObjectKind() schema.ObjectKind { return &obj.TypeMeta } diff --git a/pkg/apis/apps/v1beta1/types.go b/pkg/apis/apps/v1beta1/types.go index 32833e4a04..c76d4a5369 100644 --- a/pkg/apis/apps/v1beta1/types.go +++ b/pkg/apis/apps/v1beta1/types.go @@ -18,6 +18,7 @@ package v1beta1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/kubernetes/pkg/api/v1" ) @@ -26,6 +27,51 @@ const ( StatefulSetInitAnnotation = "pod.alpha.kubernetes.io/initialized" ) +// ScaleSpec describes the attributes of a scale subresource +type ScaleSpec struct { + // desired number of instances for the scaled object. + // +optional + Replicas int32 `json:"replicas,omitempty" protobuf:"varint,1,opt,name=replicas"` +} + +// ScaleStatus represents the current status of a scale subresource. +type ScaleStatus struct { + // actual number of observed instances of the scaled object. + Replicas int32 `json:"replicas" protobuf:"varint,1,opt,name=replicas"` + + // label query over pods that should match the replicas count. More info: http://kubernetes.io/docs/user-guide/labels#label-selectors + // +optional + Selector map[string]string `json:"selector,omitempty" protobuf:"bytes,2,rep,name=selector"` + + // label selector for pods that should match the replicas count. This is a serializated + // version of both map-based and more expressive set-based selectors. This is done to + // avoid introspection in the clients. The string will be in the same format as the + // query-param syntax. If the target type only supports map-based selectors, both this + // field and map-based selector field are populated. + // More info: http://kubernetes.io/docs/user-guide/labels#label-selectors + // +optional + TargetSelector string `json:"targetSelector,omitempty" protobuf:"bytes,3,opt,name=targetSelector"` +} + +// +genclient=true +// +noMethods=true + +// Scale represents a scaling request for a resource. +type Scale struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata; More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // defines the behavior of the scale. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status. + // +optional + Spec ScaleSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + + // current status of the scale. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status. Read-only. + // +optional + Status ScaleStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + // +genclient=true // StatefulSet represents a set of pods with consistent identities. @@ -106,3 +152,224 @@ type StatefulSetList struct { metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` Items []StatefulSet `json:"items" protobuf:"bytes,2,rep,name=items"` } + +// +genclient=true + +// 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 2. + // +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 config this deployment is rolling back to. Will be cleared after rollback is done. + // +optional + RollbackTo *RollbackConfig `json:"rollbackTo,omitempty" protobuf:"bytes,8,opt,name=rollbackTo"` + + // 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. Once autoRollback is + // implemented, the deployment controller will automatically rollback failed + // deployments. 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"` +} + +// DeploymentRollback stores the information required to rollback a deployment. +type DeploymentRollback struct { + metav1.TypeMeta `json:",inline"` + // Required: This must match the Name of a deployment. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // The annotations to be updated to a deployment + // +optional + UpdatedAnnotations map[string]string `json:"updatedAnnotations,omitempty" protobuf:"bytes,2,rep,name=updatedAnnotations"` + // The config of this deployment rollback. + RollbackTo RollbackConfig `json:"rollbackTo" protobuf:"bytes,3,opt,name=rollbackTo"` +} + +type RollbackConfig struct { + // The revision to rollback to. If set to 0, rollbck to the last revision. + // +optional + Revision int64 `json:"revision,omitempty" protobuf:"varint,1,opt,name=revision"` +} + +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. + // +optional + UnavailableReplicas int32 `json:"unavailableReplicas,omitempty" protobuf:"varint,5,opt,name=unavailableReplicas"` + + // Represents the latest available observations of a deployment's current state. + Conditions []DeploymentCondition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,6,rep,name=conditions"` +} + +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/kubernetes/pkg/api/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"` +} + +// 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"` +} diff --git a/pkg/apis/extensions/v1beta1/defaults_test.go b/pkg/apis/extensions/v1beta1/defaults_test.go index 6072b66724..3829b0071d 100644 --- a/pkg/apis/extensions/v1beta1/defaults_test.go +++ b/pkg/apis/extensions/v1beta1/defaults_test.go @@ -539,15 +539,3 @@ func newInt32(val int32) *int32 { *p = val return p } - -func newString(val string) *string { - p := new(string) - *p = val - return p -} - -func newBool(val bool) *bool { - b := new(bool) - *b = val - return b -} diff --git a/pkg/apis/extensions/validation/validation_test.go b/pkg/apis/extensions/validation/validation_test.go index b04d368597..6b76ecc1cc 100644 --- a/pkg/apis/extensions/validation/validation_test.go +++ b/pkg/apis/extensions/validation/validation_test.go @@ -2808,9 +2808,3 @@ func TestIsValidSysctlPattern(t *testing.T) { } } } - -func newBool(val bool) *bool { - p := new(bool) - *p = val - return p -} diff --git a/pkg/kubectl/cmd/apply.go b/pkg/kubectl/cmd/apply.go index 7cfc879dcf..c24f7d9d13 100644 --- a/pkg/kubectl/cmd/apply.go +++ b/pkg/kubectl/cmd/apply.go @@ -421,6 +421,7 @@ func getRESTMappings(mapper meta.RESTMapper, pruneResources *[]pruneResource) (n {"extensions", "v1beta1", "Ingress", true}, {"extensions", "v1beta1", "ReplicaSet", true}, {"apps", "v1beta1", "StatefulSet", true}, + {"apps", "v1beta1", "Deployment", true}, } } diff --git a/pkg/kubectl/cmd/util/factory_client_access.go b/pkg/kubectl/cmd/util/factory_client_access.go index ed4638dbb8..5d8cf3c461 100644 --- a/pkg/kubectl/cmd/util/factory_client_access.go +++ b/pkg/kubectl/cmd/util/factory_client_access.go @@ -552,7 +552,8 @@ func (f *ring0Factory) Generators(cmdName string) map[string]kubectl.Generator { func (f *ring0Factory) CanBeExposed(kind schema.GroupKind) error { switch kind { - case api.Kind("ReplicationController"), api.Kind("Service"), api.Kind("Pod"), extensions.Kind("Deployment"), extensions.Kind("ReplicaSet"): + case api.Kind("ReplicationController"), api.Kind("Service"), api.Kind("Pod"), + extensions.Kind("Deployment"), apps.Kind("Deployment"), extensions.Kind("ReplicaSet"): // nothing to do here default: return fmt.Errorf("cannot expose a %s", kind) @@ -562,7 +563,8 @@ func (f *ring0Factory) CanBeExposed(kind schema.GroupKind) error { func (f *ring0Factory) CanBeAutoscaled(kind schema.GroupKind) error { switch kind { - case api.Kind("ReplicationController"), extensions.Kind("Deployment"), extensions.Kind("ReplicaSet"): + case api.Kind("ReplicationController"), extensions.Kind("ReplicaSet"), + extensions.Kind("Deployment"), apps.Kind("Deployment"): // nothing to do here default: return fmt.Errorf("cannot autoscale a %v", kind) diff --git a/pkg/kubectl/history.go b/pkg/kubectl/history.go index b13491fadd..0c9398bbb8 100644 --- a/pkg/kubectl/history.go +++ b/pkg/kubectl/history.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/extensions" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" deploymentutil "k8s.io/kubernetes/pkg/controller/deployment/util" @@ -46,7 +47,7 @@ type HistoryViewer interface { func HistoryViewerFor(kind schema.GroupKind, c clientset.Interface) (HistoryViewer, error) { switch kind { - case extensions.Kind("Deployment"): + case extensions.Kind("Deployment"), apps.Kind("Deployment"): return &DeploymentHistoryViewer{c}, nil } return nil, fmt.Errorf("no history viewer has been implemented for %q", kind) diff --git a/pkg/kubectl/rollback.go b/pkg/kubectl/rollback.go index 32f9140669..f57143227f 100644 --- a/pkg/kubectl/rollback.go +++ b/pkg/kubectl/rollback.go @@ -29,6 +29,7 @@ import ( "k8s.io/apimachinery/pkg/watch" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/v1" + "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/extensions" externalextensions "k8s.io/kubernetes/pkg/apis/extensions/v1beta1" clientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" @@ -44,7 +45,7 @@ type Rollbacker interface { func RollbackerFor(kind schema.GroupKind, c clientset.Interface) (Rollbacker, error) { switch kind { - case extensions.Kind("Deployment"): + case extensions.Kind("Deployment"), apps.Kind("Deployment"): return &DeploymentRollbacker{c}, nil } return nil, fmt.Errorf("no rollbacker has been implemented for %q", kind) diff --git a/pkg/kubectl/rollout_status.go b/pkg/kubectl/rollout_status.go index af29e9ce26..2dabc26442 100644 --- a/pkg/kubectl/rollout_status.go +++ b/pkg/kubectl/rollout_status.go @@ -21,6 +21,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset" extensionsclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/extensions/internalversion" @@ -34,7 +35,7 @@ type StatusViewer interface { func StatusViewerFor(kind schema.GroupKind, c internalclientset.Interface) (StatusViewer, error) { switch kind { - case extensions.Kind("Deployment"): + case extensions.Kind("Deployment"), apps.Kind("Deployment"): return &DeploymentStatusViewer{c.Extensions()}, nil case extensions.Kind("DaemonSet"): return &DaemonSetStatusViewer{c.Extensions()}, nil diff --git a/pkg/kubectl/run.go b/pkg/kubectl/run.go index ea1eae0edc..cb9ec17c9b 100644 --- a/pkg/kubectl/run.go +++ b/pkg/kubectl/run.go @@ -26,7 +26,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/kubernetes/pkg/api" - batch "k8s.io/kubernetes/pkg/apis/batch" + "k8s.io/kubernetes/pkg/apis/batch" "k8s.io/kubernetes/pkg/apis/extensions" ) diff --git a/pkg/kubectl/scale.go b/pkg/kubectl/scale.go index 8561a1f170..21bbcfe7c8 100644 --- a/pkg/kubectl/scale.go +++ b/pkg/kubectl/scale.go @@ -60,7 +60,7 @@ func ScalerFor(kind schema.GroupKind, c internalclientset.Interface) (Scaler, er return &JobScaler{c.Batch()}, nil // Either kind of job can be scaled with Batch interface. case apps.Kind("StatefulSet"): return &StatefulSetScaler{c.Apps()}, nil - case extensions.Kind("Deployment"): + case extensions.Kind("Deployment"), apps.Kind("Deployment"): return &DeploymentScaler{c.Extensions()}, nil } return nil, fmt.Errorf("no scaler has been implemented for %q", kind) diff --git a/pkg/kubectl/stop.go b/pkg/kubectl/stop.go index 1fb7181c1f..5a70a3958e 100644 --- a/pkg/kubectl/stop.go +++ b/pkg/kubectl/stop.go @@ -90,7 +90,7 @@ func ReaperFor(kind schema.GroupKind, c internalclientset.Interface) (Reaper, er case apps.Kind("StatefulSet"): return &StatefulSetReaper{c.Apps(), c.Core(), Interval, Timeout}, nil - case extensions.Kind("Deployment"): + case extensions.Kind("Deployment"), apps.Kind("Deployment"): return &DeploymentReaper{c.Extensions(), c.Extensions(), Interval, Timeout}, nil } diff --git a/pkg/master/master.go b/pkg/master/master.go index 6f44d4e529..66c0f5757d 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -33,7 +33,7 @@ import ( "k8s.io/kubernetes/cmd/kube-apiserver/app/options" "k8s.io/kubernetes/pkg/api" apiv1 "k8s.io/kubernetes/pkg/api/v1" - appsapi "k8s.io/kubernetes/pkg/apis/apps/v1beta1" + appsv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1" authenticationv1 "k8s.io/kubernetes/pkg/apis/authentication/v1" authenticationv1beta1 "k8s.io/kubernetes/pkg/apis/authentication/v1beta1" authorizationapiv1 "k8s.io/kubernetes/pkg/apis/authorization/v1" @@ -367,7 +367,7 @@ func DefaultAPIResourceConfigSource() *serverstorage.ResourceConfig { authenticationv1.SchemeGroupVersion, authenticationv1beta1.SchemeGroupVersion, autoscalingapiv1.SchemeGroupVersion, - appsapi.SchemeGroupVersion, + appsv1beta1.SchemeGroupVersion, policyapiv1beta1.SchemeGroupVersion, rbacv1beta1.SchemeGroupVersion, rbacapi.SchemeGroupVersion, diff --git a/pkg/printers/internalversion/describe.go b/pkg/printers/internalversion/describe.go index 3a43dae9d2..4baec3c7cb 100644 --- a/pkg/printers/internalversion/describe.go +++ b/pkg/printers/internalversion/describe.go @@ -118,6 +118,7 @@ func describerMap(c clientset.Interface) map[schema.GroupKind]printers.Describer batch.Kind("Job"): &JobDescriber{c}, batch.Kind("CronJob"): &CronJobDescriber{c}, apps.Kind("StatefulSet"): &StatefulSetDescriber{c}, + apps.Kind("Deployment"): &DeploymentDescriber{c, versionedClientsetForDeployment(c)}, certificates.Kind("CertificateSigningRequest"): &CertificateSigningRequestDescriber{c}, storage.Kind("StorageClass"): &StorageClassDescriber{c}, policy.Kind("PodDisruptionBudget"): &PodDisruptionBudgetDescriber{c}, diff --git a/pkg/registry/apps/rest/storage_apps.go b/pkg/registry/apps/rest/storage_apps.go index 66dffbd94e..fdf5e3a71c 100644 --- a/pkg/registry/apps/rest/storage_apps.go +++ b/pkg/registry/apps/rest/storage_apps.go @@ -25,6 +25,7 @@ import ( "k8s.io/kubernetes/pkg/apis/apps" appsapiv1beta1 "k8s.io/kubernetes/pkg/apis/apps/v1beta1" statefulsetstore "k8s.io/kubernetes/pkg/registry/apps/petset/storage" + deploymentstore "k8s.io/kubernetes/pkg/registry/extensions/deployment/storage" ) type RESTStorageProvider struct{} @@ -44,6 +45,13 @@ func (p RESTStorageProvider) v1beta1Storage(apiResourceConfigSource serverstorag version := appsapiv1beta1.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 + storage["deployments/rollback"] = deploymentStorage.Rollback + storage["deployments/scale"] = deploymentStorage.Scale + } if apiResourceConfigSource.ResourceEnabled(version.WithResource("statefulsets")) { statefulsetStorage, statefulsetStatusStorage := statefulsetstore.NewREST(restOptionsGetter) storage["statefulsets"] = statefulsetStorage diff --git a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/controller_policy.go b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/controller_policy.go index 771b6a1831..7c9fb23272 100644 --- a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/controller_policy.go +++ b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/controller_policy.go @@ -92,8 +92,8 @@ func init() { addControllerRole(rbac.ClusterRole{ ObjectMeta: metav1.ObjectMeta{Name: saRolePrefix + "deployment-controller"}, Rules: []rbac.PolicyRule{ - rbac.NewRule("get", "list", "watch", "update").Groups(extensionsGroup).Resources("deployments").RuleOrDie(), - rbac.NewRule("update").Groups(extensionsGroup).Resources("deployments/status").RuleOrDie(), + rbac.NewRule("get", "list", "watch", "update").Groups(extensionsGroup, appsGroup).Resources("deployments").RuleOrDie(), + rbac.NewRule("update").Groups(extensionsGroup, appsGroup).Resources("deployments/status").RuleOrDie(), rbac.NewRule("get", "list", "watch", "create", "update", "patch", "delete").Groups(extensionsGroup).Resources("replicasets").RuleOrDie(), // TODO: remove "update" once // https://github.com/kubernetes/kubernetes/issues/36897 is resolved. @@ -104,7 +104,7 @@ func init() { addControllerRole(rbac.ClusterRole{ ObjectMeta: metav1.ObjectMeta{Name: saRolePrefix + "disruption-controller"}, Rules: []rbac.PolicyRule{ - rbac.NewRule("get", "list", "watch").Groups(extensionsGroup).Resources("deployments").RuleOrDie(), + rbac.NewRule("get", "list", "watch").Groups(extensionsGroup, appsGroup).Resources("deployments").RuleOrDie(), rbac.NewRule("get", "list", "watch").Groups(extensionsGroup).Resources("replicasets").RuleOrDie(), rbac.NewRule("get", "list", "watch").Groups(legacyGroup).Resources("replicationcontrollers").RuleOrDie(), rbac.NewRule("get", "list", "watch").Groups(policyGroup).Resources("poddisruptionbudgets").RuleOrDie(), @@ -138,7 +138,7 @@ func init() { rbac.NewRule("get", "update").Groups(legacyGroup).Resources("replicationcontrollers/scale").RuleOrDie(), // TODO this should be removable when the HPA contoller is fixed rbac.NewRule("get", "update").Groups(extensionsGroup).Resources("replicationcontrollers/scale").RuleOrDie(), - rbac.NewRule("get", "update").Groups(extensionsGroup).Resources("deployments/scale", "replicasets/scale").RuleOrDie(), + rbac.NewRule("get", "update").Groups(extensionsGroup, appsGroup).Resources("deployments/scale", "replicasets/scale").RuleOrDie(), rbac.NewRule("list").Groups(legacyGroup).Resources("pods").RuleOrDie(), // TODO: Remove the root /proxy permission in 1.7; MetricsClient no longer requires root proxy access as of 1.6 (fixed in https://github.com/kubernetes/kubernetes/pull/39636) rbac.NewRule("proxy").Groups(legacyGroup).Resources("services").Names("https:heapster:", "http:heapster:").RuleOrDie(), diff --git a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go index d0e03e6ca0..892c989fb2 100644 --- a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go +++ b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go @@ -127,14 +127,16 @@ func ClusterRoles() []rbac.ClusterRole { rbac.NewRule(Read...).Groups(legacyGroup).Resources("namespaces").RuleOrDie(), rbac.NewRule("impersonate").Groups(legacyGroup).Resources("serviceaccounts").RuleOrDie(), - rbac.NewRule(ReadWrite...).Groups(appsGroup).Resources("statefulsets").RuleOrDie(), + rbac.NewRule(ReadWrite...).Groups(appsGroup).Resources("statefulsets", + "deployments", "deployments/scale", "deployments/rollback").RuleOrDie(), rbac.NewRule(ReadWrite...).Groups(autoscalingGroup).Resources("horizontalpodautoscalers").RuleOrDie(), rbac.NewRule(ReadWrite...).Groups(batchGroup).Resources("jobs", "cronjobs", "scheduledjobs").RuleOrDie(), - rbac.NewRule(ReadWrite...).Groups(extensionsGroup).Resources("daemonsets", "deployments", "deployments/scale", - "ingresses", "replicasets", "replicasets/scale", "replicationcontrollers/scale").RuleOrDie(), + rbac.NewRule(ReadWrite...).Groups(extensionsGroup).Resources("daemonsets", + "deployments", "deployments/scale", "deployments/rollback", "ingresses", + "replicasets", "replicasets/scale", "replicationcontrollers/scale").RuleOrDie(), // additional admin powers rbac.NewRule("create").Groups(authorizationGroup).Resources("localsubjectaccessreviews").RuleOrDie(), @@ -157,14 +159,16 @@ func ClusterRoles() []rbac.ClusterRole { rbac.NewRule(Read...).Groups(legacyGroup).Resources("namespaces").RuleOrDie(), rbac.NewRule("impersonate").Groups(legacyGroup).Resources("serviceaccounts").RuleOrDie(), - rbac.NewRule(ReadWrite...).Groups(appsGroup).Resources("statefulsets").RuleOrDie(), + rbac.NewRule(ReadWrite...).Groups(appsGroup).Resources("statefulsets", + "deployments", "deployments/scale", "deployments/rollback").RuleOrDie(), rbac.NewRule(ReadWrite...).Groups(autoscalingGroup).Resources("horizontalpodautoscalers").RuleOrDie(), rbac.NewRule(ReadWrite...).Groups(batchGroup).Resources("jobs", "cronjobs", "scheduledjobs").RuleOrDie(), - rbac.NewRule(ReadWrite...).Groups(extensionsGroup).Resources("daemonsets", "deployments", "deployments/scale", - "ingresses", "replicasets", "replicasets/scale", "replicationcontrollers/scale").RuleOrDie(), + rbac.NewRule(ReadWrite...).Groups(extensionsGroup).Resources("daemonsets", + "deployments", "deployments/scale", "deployments/rollback", "ingresses", + "replicasets", "replicasets/scale", "replicationcontrollers/scale").RuleOrDie(), }, }, { @@ -180,7 +184,7 @@ func ClusterRoles() []rbac.ClusterRole { // indicator of which namespaces you have access to. rbac.NewRule(Read...).Groups(legacyGroup).Resources("namespaces").RuleOrDie(), - rbac.NewRule(Read...).Groups(appsGroup).Resources("statefulsets").RuleOrDie(), + rbac.NewRule(Read...).Groups(appsGroup).Resources("statefulsets", "deployments", "deployments/scale").RuleOrDie(), rbac.NewRule(Read...).Groups(autoscalingGroup).Resources("horizontalpodautoscalers").RuleOrDie(), @@ -320,6 +324,7 @@ func ClusterRoles() []rbac.ClusterRole { "podsecuritypolicies", "replicasets", ).RuleOrDie(), + rbac.NewRule("list", "watch").Groups(appsGroup).Resources("deployments").RuleOrDie(), rbac.NewRule("list", "watch").Groups(batchGroup).Resources("jobs", "cronjobs").RuleOrDie(), rbac.NewRule("list", "watch").Groups(appsGroup).Resources("statefulsets").RuleOrDie(), rbac.NewRule("list", "watch").Groups(policyGroup).Resources("poddisruptionbudgets").RuleOrDie(), diff --git a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy_test.go b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy_test.go index 22d0d2532a..3d944da94f 100644 --- a/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy_test.go +++ b/plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy_test.go @@ -117,6 +117,12 @@ var viewEscalatingNamespaceResources = []rbac.PolicyRule{ rbac.NewRule(bootstrappolicy.Read...).Groups("").Resources("services/proxy").RuleOrDie(), } +// ungettableResources is the list of rules that don't allow to view (GET) them +// this is purposefully separate list to distinguish from escalating privs +var ungettableResources = []rbac.PolicyRule{ + rbac.NewRule(bootstrappolicy.Read...).Groups("apps", "extensions").Resources("deployments/rollback").RuleOrDie(), +} + func TestEditViewRelationship(t *testing.T) { readVerbs := sets.NewString(bootstrappolicy.Read...) semanticRoles := getSemanticRoles(bootstrappolicy.ClusterRoles()) @@ -142,6 +148,14 @@ func TestEditViewRelationship(t *testing.T) { } semanticRoles.view.Rules = append(semanticRoles.view.Rules, viewEscalatingNamespaceResources...) + // confirm that the view role doesn't have ungettable resources + for _, rule := range ungettableResources { + if covers, _ := rbacregistryvalidation.Covers(semanticRoles.view.Rules, []rbac.PolicyRule{rule}); covers { + t.Errorf("view has ungettable resource: %#v", rule) + } + } + semanticRoles.view.Rules = append(semanticRoles.view.Rules, ungettableResources...) + // at this point, we should have a two way covers relationship if covers, miss := rbacregistryvalidation.Covers(semanticRoles.edit.Rules, semanticRoles.view.Rules); !covers { t.Errorf("edit has lost rules for: %#v", miss) diff --git a/test/e2e_federation/deployment.go b/test/e2e_federation/deployment.go index f98523a5c5..5aece070cd 100644 --- a/test/e2e_federation/deployment.go +++ b/test/e2e_federation/deployment.go @@ -193,7 +193,7 @@ func waitForDeployment(c *fedclientset.Clientset, namespace string, deploymentNa } specReplicas, statusReplicas := int32(0), int32(0) for _, cluster := range clusters { - dep, err := cluster.Deployments(namespace).Get(deploymentName, metav1.GetOptions{}) + dep, err := cluster.Extensions().Deployments(namespace).Get(deploymentName, metav1.GetOptions{}) if err != nil && !errors.IsNotFound(err) { By(fmt.Sprintf("Failed getting deployment: %q/%q/%q, err: %v", cluster.name, namespace, deploymentName, err)) return false, err diff --git a/test/integration/master/master_test.go b/test/integration/master/master_test.go index f839d6906d..cbf25d15f1 100644 --- a/test/integration/master/master_test.go +++ b/test/integration/master/master_test.go @@ -138,6 +138,60 @@ var hpaV1 string = ` } ` +var deploymentExtensions string = ` +{ + "apiVersion": "extensions/v1beta1", + "kind": "Deployment", + "metadata": { + "name": "test-deployment1", + "namespace": "default" + }, + "spec": { + "replicas": 1, + "template": { + "metadata": { + "labels": { + "app": "nginx0" + } + }, + "spec": { + "containers": [{ + "name": "nginx", + "image": "gcr.io/google-containers/nginx:1.7.9" + }] + } + } + } +} +` + +var deploymentApps string = ` +{ + "apiVersion": "apps/v1beta1", + "kind": "Deployment", + "metadata": { + "name": "test-deployment2", + "namespace": "default" + }, + "spec": { + "replicas": 1, + "template": { + "metadata": { + "labels": { + "app": "nginx0" + } + }, + "spec": { + "containers": [{ + "name": "nginx", + "image": "gcr.io/google-containers/nginx:1.7.9" + }] + } + } + } +} +` + func autoscalingPath(resource, namespace, name string) string { return testapi.Autoscaling.ResourcePath(resource, namespace, name) } @@ -150,6 +204,10 @@ func extensionsPath(resource, namespace, name string) string { return testapi.Extensions.ResourcePath(resource, namespace, name) } +func appsPath(resource, namespace, name string) string { + return testapi.Apps.ResourcePath(resource, namespace, name) +} + func TestAutoscalingGroupBackwardCompatibility(t *testing.T) { _, s := framework.RunAMaster(nil) defer s.Close() @@ -195,6 +253,59 @@ func TestAutoscalingGroupBackwardCompatibility(t *testing.T) { } } +func TestAppsGroupBackwardCompatibility(t *testing.T) { + _, s := framework.RunAMaster(nil) + defer s.Close() + transport := http.DefaultTransport + + requests := []struct { + verb string + URL string + body string + expectedStatusCodes map[int]bool + expectedVersion string + }{ + // Post to extensions endpoint and get back from both: extensions and apps + {"POST", extensionsPath("deployments", metav1.NamespaceDefault, ""), deploymentExtensions, integration.Code201, ""}, + {"GET", extensionsPath("deployments", metav1.NamespaceDefault, "test-deployment1"), "", integration.Code200, testapi.Extensions.GroupVersion().String()}, + {"GET", appsPath("deployments", metav1.NamespaceDefault, "test-deployment1"), "", integration.Code200, testapi.Apps.GroupVersion().String()}, + {"DELETE", extensionsPath("deployments", metav1.NamespaceDefault, "test-deployment1"), "", integration.Code200, testapi.Extensions.GroupVersion().String()}, + // Post to apps endpoint and get back from both: apps and extensions + {"POST", appsPath("deployments", metav1.NamespaceDefault, ""), deploymentApps, integration.Code201, ""}, + {"GET", appsPath("deployments", metav1.NamespaceDefault, "test-deployment2"), "", integration.Code200, testapi.Apps.GroupVersion().String()}, + {"GET", extensionsPath("deployments", metav1.NamespaceDefault, "test-deployment2"), "", integration.Code200, testapi.Extensions.GroupVersion().String()}, + {"DELETE", appsPath("deployments", metav1.NamespaceDefault, "test-deployment2"), "", integration.Code200, testapi.Apps.GroupVersion().String()}, + } + + for _, r := range requests { + bodyBytes := bytes.NewReader([]byte(r.body)) + req, err := http.NewRequest(r.verb, s.URL+r.URL, bodyBytes) + if err != nil { + t.Logf("case %v", r) + t.Fatalf("unexpected error: %v", err) + } + func() { + resp, err := transport.RoundTrip(req) + defer resp.Body.Close() + if err != nil { + t.Logf("case %v", r) + t.Fatalf("unexpected error: %v", err) + } + b, _ := ioutil.ReadAll(resp.Body) + body := string(b) + if _, ok := r.expectedStatusCodes[resp.StatusCode]; !ok { + t.Logf("case %v", r) + t.Errorf("Expected status one of %v, but got %v", r.expectedStatusCodes, resp.StatusCode) + t.Errorf("Body: %v", body) + } + if !strings.Contains(body, "\"apiVersion\":\""+r.expectedVersion) { + t.Logf("case %v", r) + t.Errorf("Expected version %v, got body %v", r.expectedVersion, body) + } + }() + } +} + func TestAccept(t *testing.T) { _, s := framework.RunAMaster(nil) defer s.Close()