diff --git a/cmd/kube-apiserver/app/server.go b/cmd/kube-apiserver/app/server.go index abf3ac5b4cb..ccae17c608d 100644 --- a/cmd/kube-apiserver/app/server.go +++ b/cmd/kube-apiserver/app/server.go @@ -558,9 +558,10 @@ func BuildStorageFactory(s *options.ServerRunOptions) (*serverstorage.DefaultSto return nil, fmt.Errorf("error in initializing storage factory: %s", err) } - // keep Deployments, NetworkPolicies and Daemonsets in extensions for backwards compatibility, we'll have to migrate at some point, eventually + // keep Deployments, NetworkPolicies, Daemonsets and ReplicaSets in extensions for backwards compatibility, we'll have to migrate at some point, eventually storageFactory.AddCohabitatingResources(extensions.Resource("deployments"), apps.Resource("deployments")) storageFactory.AddCohabitatingResources(extensions.Resource("daemonsets"), apps.Resource("daemonsets")) + storageFactory.AddCohabitatingResources(extensions.Resource("replicasets"), apps.Resource("replicasets")) storageFactory.AddCohabitatingResources(extensions.Resource("networkpolicies"), networking.Resource("networkpolicies")) for _, override := range s.Etcd.EtcdServersOverrides { tokens := strings.Split(override, "#") diff --git a/pkg/api/defaulting_test.go b/pkg/api/defaulting_test.go index 6f9f14730db..562f1fda8b5 100644 --- a/pkg/api/defaulting_test.go +++ b/pkg/api/defaulting_test.go @@ -116,6 +116,8 @@ 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: "v1beta2", Kind: "ReplicaSet"}: {}, + {Group: "apps", Version: "v1beta2", Kind: "ReplicaSetList"}: {}, {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/apis/apps/register.go b/pkg/apis/apps/register.go index a5baec0a004..77d8445ae14 100644 --- a/pkg/apis/apps/register.go +++ b/pkg/apis/apps/register.go @@ -57,6 +57,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &StatefulSetList{}, &ControllerRevision{}, &ControllerRevisionList{}, + &extensions.ReplicaSet{}, + &extensions.ReplicaSetList{}, ) return nil } diff --git a/pkg/apis/apps/v1beta2/conversion.go b/pkg/apis/apps/v1beta2/conversion.go index a41cd39f8a9..44bfc751102 100644 --- a/pkg/apis/apps/v1beta2/conversion.go +++ b/pkg/apis/apps/v1beta2/conversion.go @@ -54,6 +54,8 @@ func addConversionFuncs(scheme *runtime.Scheme) error { Convert_extensions_DeploymentStrategy_To_v1beta2_DeploymentStrategy, Convert_v1beta2_RollingUpdateDeployment_To_extensions_RollingUpdateDeployment, Convert_extensions_RollingUpdateDeployment_To_v1beta2_RollingUpdateDeployment, + Convert_extensions_ReplicaSetSpec_To_v1beta2_ReplicaSetSpec, + Convert_v1beta2_ReplicaSetSpec_To_extensions_ReplicaSetSpec, ) if err != nil { return err @@ -84,6 +86,18 @@ func addConversionFuncs(scheme *runtime.Scheme) error { if err != nil { return err } + err = scheme.AddFieldLabelConversionFunc("apps/v1beta2", "ReplicaSet", + 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 appsv1beta2.ReplicaSet", label) + } + }) + if err != nil { + return err + } return nil } @@ -361,3 +375,26 @@ func Convert_extensions_RollingUpdateDeployment_To_v1beta2_RollingUpdateDeployme } return nil } + +func Convert_extensions_ReplicaSetSpec_To_v1beta2_ReplicaSetSpec(in *extensions.ReplicaSetSpec, out *appsv1beta2.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_v1beta2_ReplicaSetSpec_To_extensions_ReplicaSetSpec(in *appsv1beta2.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 +} diff --git a/pkg/apis/apps/v1beta2/defaults.go b/pkg/apis/apps/v1beta2/defaults.go index f7c62d1fe2a..102750339cd 100644 --- a/pkg/apis/apps/v1beta2/defaults.go +++ b/pkg/apis/apps/v1beta2/defaults.go @@ -151,3 +151,23 @@ func SetDefaults_Deployment(obj *appsv1beta2.Deployment) { *obj.Spec.ProgressDeadlineSeconds = 600 } } + +func SetDefaults_ReplicaSet(obj *appsv1beta2.ReplicaSet) { + labels := obj.Spec.Template.Labels + + // TODO: support templates defined elsewhere when we support them in the API + if labels != nil { + if obj.Spec.Selector == nil { + obj.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: labels, + } + } + if len(obj.Labels) == 0 { + obj.Labels = labels + } + } + if obj.Spec.Replicas == nil { + obj.Spec.Replicas = new(int32) + *obj.Spec.Replicas = 1 + } +} diff --git a/pkg/apis/apps/v1beta2/defaults_test.go b/pkg/apis/apps/v1beta2/defaults_test.go index 5cd1491905a..6a9ba308f14 100644 --- a/pkg/apis/apps/v1beta2/defaults_test.go +++ b/pkg/apis/apps/v1beta2/defaults_test.go @@ -24,6 +24,7 @@ import ( "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" @@ -319,6 +320,219 @@ func TestDefaultDeploymentAvailability(t *testing.T) { } } +func TestSetDefaultReplicaSet(t *testing.T) { + tests := []struct { + rs *appsv1beta2.ReplicaSet + expectLabels bool + expectSelector bool + }{ + { + rs: &appsv1beta2.ReplicaSet{ + Spec: appsv1beta2.ReplicaSetSpec{ + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectLabels: true, + expectSelector: true, + }, + { + rs: &appsv1beta2.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "bar": "foo", + }, + }, + Spec: appsv1beta2.ReplicaSetSpec{ + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectLabels: false, + expectSelector: true, + }, + { + rs: &appsv1beta2.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "bar": "foo", + }, + }, + Spec: appsv1beta2.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "some": "other", + }, + }, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectLabels: false, + expectSelector: false, + }, + { + rs: &appsv1beta2.ReplicaSet{ + Spec: appsv1beta2.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "some": "other", + }, + }, + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectLabels: true, + expectSelector: false, + }, + } + + for _, test := range tests { + rs := test.rs + obj2 := roundTrip(t, runtime.Object(rs)) + rs2, ok := obj2.(*appsv1beta2.ReplicaSet) + if !ok { + t.Errorf("unexpected object: %v", rs2) + t.FailNow() + } + if test.expectSelector != reflect.DeepEqual(rs2.Spec.Selector.MatchLabels, rs2.Spec.Template.Labels) { + if test.expectSelector { + t.Errorf("expected: %v, got: %v", rs2.Spec.Template.Labels, rs2.Spec.Selector) + } else { + t.Errorf("unexpected equality: %v", rs.Spec.Selector) + } + } + if test.expectLabels != reflect.DeepEqual(rs2.Labels, rs2.Spec.Template.Labels) { + if test.expectLabels { + t.Errorf("expected: %v, got: %v", rs2.Spec.Template.Labels, rs2.Labels) + } else { + t.Errorf("unexpected equality: %v", rs.Labels) + } + } + } +} + +func TestSetDefaultReplicaSetReplicas(t *testing.T) { + tests := []struct { + rs appsv1beta2.ReplicaSet + expectReplicas int32 + }{ + { + rs: appsv1beta2.ReplicaSet{ + Spec: appsv1beta2.ReplicaSetSpec{ + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectReplicas: 1, + }, + { + rs: appsv1beta2.ReplicaSet{ + Spec: appsv1beta2.ReplicaSetSpec{ + Replicas: newInt32(0), + Template: v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectReplicas: 0, + }, + { + rs: appsv1beta2.ReplicaSet{ + Spec: appsv1beta2.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.(*appsv1beta2.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 := &appsv1beta2.ReplicaSet{ + Spec: appsv1beta2.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.(*appsv1beta2.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(api.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 52774ad83f7..ff82d94c240 100644 --- a/pkg/registry/apps/rest/storage_apps.go +++ b/pkg/registry/apps/rest/storage_apps.go @@ -29,6 +29,7 @@ import ( statefulsetstore "k8s.io/kubernetes/pkg/registry/apps/statefulset/storage" daemonsetstore "k8s.io/kubernetes/pkg/registry/extensions/daemonset/storage" deploymentstore "k8s.io/kubernetes/pkg/registry/extensions/deployment/storage" + replicasetstore "k8s.io/kubernetes/pkg/registry/extensions/replicaset/storage" ) type RESTStorageProvider struct{} @@ -94,6 +95,12 @@ func (p RESTStorageProvider) v1beta2Storage(apiResourceConfigSource serverstorag 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 + storage["replicasets/scale"] = replicaSetStorage.Scale + } return storage } diff --git a/staging/src/k8s.io/api/apps/v1beta2/register.go b/staging/src/k8s.io/api/apps/v1beta2/register.go index ca323f66b69..6643bea625f 100644 --- a/staging/src/k8s.io/api/apps/v1beta2/register.go +++ b/staging/src/k8s.io/api/apps/v1beta2/register.go @@ -52,6 +52,8 @@ func addKnownTypes(scheme *runtime.Scheme) error { &StatefulSetList{}, &DaemonSet{}, &DaemonSetList{}, + &ReplicaSet{}, + &ReplicaSetList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/staging/src/k8s.io/api/apps/v1beta2/types.go b/staging/src/k8s.io/api/apps/v1beta2/types.go index 5a018c62485..417f996cb67 100644 --- a/staging/src/k8s.io/api/apps/v1beta2/types.go +++ b/staging/src/k8s.io/api/apps/v1beta2/types.go @@ -694,3 +694,135 @@ 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 + +// WIP: This is not ready to be used and we plan to make breaking changes to it. +// ReplicaSet represents the configuration of a ReplicaSet. +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 + +// WIP: This is not ready to be used and we plan to make breaking changes to it. +// 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"` +} + +// WIP: This is not ready to be used and we plan to make breaking changes to it. +// 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"` +} + +// WIP: This is not ready to be used and we plan to make breaking changes to it. +// 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" +) + +// WIP: This is not ready to be used and we plan to make breaking changes to it. +// 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"` +} diff --git a/test/integration/etcd/etcd_storage_path_test.go b/test/integration/etcd/etcd_storage_path_test.go index fa19f9fcce6..8ccfa5bdc62 100644 --- a/test/integration/etcd/etcd_storage_path_test.go +++ b/test/integration/etcd/etcd_storage_path_test.go @@ -163,6 +163,11 @@ var etcdStorageData = map[schema.GroupVersionResource]struct { expectedEtcdPath: "/registry/daemonsets/etcdstoragepathtestnamespace/ds5", expectedGVK: gvkP("extensions", "v1beta1", "DaemonSet"), }, + gvr("apps", "v1beta2", "replicasets"): { + stub: `{"metadata": {"name": "rs2"}, "spec": {"selector": {"matchLabels": {"g": "h"}}, "template": {"metadata": {"labels": {"g": "h"}}, "spec": {"containers": [{"image": "fedora:latest", "name": "container4"}]}}}}`, + expectedEtcdPath: "/registry/replicasets/etcdstoragepathtestnamespace/rs2", + expectedGVK: gvkP("extensions", "v1beta1", "ReplicaSet"), + }, // -- // k8s.io/kubernetes/pkg/apis/autoscaling/v1