diff --git a/pkg/controlplane/apiserver/aggregator.go b/pkg/controlplane/apiserver/aggregator.go index ef3b58871b8..087896a3554 100644 --- a/pkg/controlplane/apiserver/aggregator.go +++ b/pkg/controlplane/apiserver/aggregator.go @@ -284,6 +284,7 @@ func DefaultGenericAPIServicePriorities() map[schema.GroupVersion]APIServicePrio {Group: "admissionregistration.k8s.io", Version: "v1beta1"}: {Group: 16700, Version: 12}, {Group: "admissionregistration.k8s.io", Version: "v1alpha1"}: {Group: 16700, Version: 9}, {Group: "coordination.k8s.io", Version: "v1"}: {Group: 16500, Version: 15}, + {Group: "coordination.k8s.io", Version: "v1alpha1"}: {Group: 16500, Version: 9}, {Group: "discovery.k8s.io", Version: "v1"}: {Group: 16200, Version: 15}, {Group: "discovery.k8s.io", Version: "v1beta1"}: {Group: 16200, Version: 12}, {Group: "flowcontrol.apiserver.k8s.io", Version: "v1"}: {Group: 16100, Version: 21}, diff --git a/pkg/controlplane/instance.go b/pkg/controlplane/instance.go index 8f4d9ff3a2e..62ebf0a8395 100644 --- a/pkg/controlplane/instance.go +++ b/pkg/controlplane/instance.go @@ -38,6 +38,7 @@ import ( certificatesapiv1 "k8s.io/api/certificates/v1" certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1" coordinationapiv1 "k8s.io/api/coordination/v1" + coordinationv1alpha1 "k8s.io/api/coordination/v1alpha1" apiv1 "k8s.io/api/core/v1" discoveryv1 "k8s.io/api/discovery/v1" eventsv1 "k8s.io/api/events/v1" @@ -475,6 +476,8 @@ var ( admissionregistrationv1alpha1.SchemeGroupVersion, apiserverinternalv1alpha1.SchemeGroupVersion, authenticationv1alpha1.SchemeGroupVersion, + apiserverinternalv1alpha1.SchemeGroupVersion, + coordinationv1alpha1.SchemeGroupVersion, resourceapi.SchemeGroupVersion, certificatesv1alpha1.SchemeGroupVersion, networkingapiv1alpha1.SchemeGroupVersion, diff --git a/pkg/kubeapiserver/default_storage_factory_builder.go b/pkg/kubeapiserver/default_storage_factory_builder.go index e7228aef57b..9dc7d74eacb 100644 --- a/pkg/kubeapiserver/default_storage_factory_builder.go +++ b/pkg/kubeapiserver/default_storage_factory_builder.go @@ -29,6 +29,7 @@ import ( "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/apps" "k8s.io/kubernetes/pkg/apis/certificates" + "k8s.io/kubernetes/pkg/apis/coordination" api "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/apis/events" "k8s.io/kubernetes/pkg/apis/extensions" @@ -71,6 +72,7 @@ func NewStorageFactoryConfig() *StorageFactoryConfig { // // TODO (https://github.com/kubernetes/kubernetes/issues/108451): remove the override in 1.25. // apisstorage.Resource("csistoragecapacities").WithVersion("v1beta1"), + coordination.Resource("leasecandidates").WithVersion("v1alpha1"), networking.Resource("ipaddresses").WithVersion("v1beta1"), networking.Resource("servicecidrs").WithVersion("v1beta1"), certificates.Resource("clustertrustbundles").WithVersion("v1alpha1"), diff --git a/pkg/printers/internalversion/printers.go b/pkg/printers/internalversion/printers.go index f4cc06ac0f3..37f4291a448 100644 --- a/pkg/printers/internalversion/printers.go +++ b/pkg/printers/internalversion/printers.go @@ -34,6 +34,7 @@ import ( certificatesv1alpha1 "k8s.io/api/certificates/v1alpha1" certificatesv1beta1 "k8s.io/api/certificates/v1beta1" coordinationv1 "k8s.io/api/coordination/v1" + coordinationv1alpha1 "k8s.io/api/coordination/v1alpha1" apiv1 "k8s.io/api/core/v1" discoveryv1beta1 "k8s.io/api/discovery/v1beta1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" @@ -51,6 +52,7 @@ import ( "k8s.io/apimachinery/pkg/util/duration" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/util/certificate/csr" + podutil "k8s.io/kubernetes/pkg/api/v1/pod" "k8s.io/kubernetes/pkg/apis/admissionregistration" "k8s.io/kubernetes/pkg/apis/apiserverinternal" @@ -430,6 +432,16 @@ func AddHandlers(h printers.PrintHandler) { _ = h.TableHandler(leaseColumnDefinitions, printLease) _ = h.TableHandler(leaseColumnDefinitions, printLeaseList) + leaseCandidateColumnDefinitions := []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, + {Name: "LeaseName", Type: "string", Description: coordinationv1alpha1.LeaseCandidateSpec{}.SwaggerDoc()["leaseName"]}, + {Name: "BinaryVersion", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["binaryVersion"]}, + {Name: "EmulationVersion", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["emulationVersion"]}, + {Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]}, + } + _ = h.TableHandler(leaseCandidateColumnDefinitions, printLeaseCandidate) + _ = h.TableHandler(leaseCandidateColumnDefinitions, printLeaseCandidateList) + storageClassColumnDefinitions := []metav1.TableColumnDefinition{ {Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, {Name: "Provisioner", Type: "string", Description: storagev1.StorageClass{}.SwaggerDoc()["provisioner"]}, @@ -2567,6 +2579,27 @@ func printLeaseList(list *coordination.LeaseList, options printers.GenerateOptio return rows, nil } +func printLeaseCandidate(obj *coordination.LeaseCandidate, options printers.GenerateOptions) ([]metav1.TableRow, error) { + row := metav1.TableRow{ + Object: runtime.RawExtension{Object: obj}, + } + + row.Cells = append(row.Cells, obj.Name, obj.Spec.LeaseName, obj.Spec.BinaryVersion, obj.Spec.EmulationVersion, translateTimestampSince(obj.CreationTimestamp)) + return []metav1.TableRow{row}, nil +} + +func printLeaseCandidateList(list *coordination.LeaseCandidateList, options printers.GenerateOptions) ([]metav1.TableRow, error) { + rows := make([]metav1.TableRow, 0, len(list.Items)) + for i := range list.Items { + r, err := printLeaseCandidate(&list.Items[i], options) + if err != nil { + return nil, err + } + rows = append(rows, r...) + } + return rows, nil +} + func printStatus(obj *metav1.Status, options printers.GenerateOptions) ([]metav1.TableRow, error) { row := metav1.TableRow{ Object: runtime.RawExtension{Object: obj}, diff --git a/pkg/registry/coordination/lease/strategy.go b/pkg/registry/coordination/lease/strategy.go index 042a3a9d7f8..2af5cf460e3 100644 --- a/pkg/registry/coordination/lease/strategy.go +++ b/pkg/registry/coordination/lease/strategy.go @@ -22,9 +22,11 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apiserver/pkg/storage/names" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/coordination" "k8s.io/kubernetes/pkg/apis/coordination/validation" + "k8s.io/kubernetes/pkg/features" ) // leaseStrategy implements verification logic for Leases. @@ -43,10 +45,26 @@ func (leaseStrategy) NamespaceScoped() bool { // PrepareForCreate prepares Lease for creation. func (leaseStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { + lease := obj.(*coordination.Lease) + if !utilfeature.DefaultFeatureGate.Enabled(features.CoordinatedLeaderElection) { + lease.Spec.Strategy = nil + lease.Spec.PreferredHolder = nil + } + } // PrepareForUpdate clears fields that are not allowed to be set by end users on update. func (leaseStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { + oldLease := old.(*coordination.Lease) + newLease := obj.(*coordination.Lease) + if !utilfeature.DefaultFeatureGate.Enabled(features.CoordinatedLeaderElection) { + if oldLease == nil || oldLease.Spec.Strategy == nil { + newLease.Spec.Strategy = nil + } + if oldLease == nil || oldLease.Spec.PreferredHolder == nil { + newLease.Spec.PreferredHolder = nil + } + } } // Validate validates a new Lease. diff --git a/pkg/registry/coordination/leasecandidate/doc.go b/pkg/registry/coordination/leasecandidate/doc.go new file mode 100644 index 00000000000..930164f918d --- /dev/null +++ b/pkg/registry/coordination/leasecandidate/doc.go @@ -0,0 +1,17 @@ +/* +Copyright 2024 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 leasecandidate diff --git a/pkg/registry/coordination/leasecandidate/storage/storage.go b/pkg/registry/coordination/leasecandidate/storage/storage.go new file mode 100644 index 00000000000..be1b84e414f --- /dev/null +++ b/pkg/registry/coordination/leasecandidate/storage/storage.go @@ -0,0 +1,56 @@ +/* +Copyright 2024 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 storage + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic" + genericregistry "k8s.io/apiserver/pkg/registry/generic/registry" + + coordinationapi "k8s.io/kubernetes/pkg/apis/coordination" + "k8s.io/kubernetes/pkg/printers" + printersinternal "k8s.io/kubernetes/pkg/printers/internalversion" + printerstorage "k8s.io/kubernetes/pkg/printers/storage" + "k8s.io/kubernetes/pkg/registry/coordination/leasecandidate" +) + +// REST implements a RESTStorage for leasecandidates against etcd +type REST struct { + *genericregistry.Store +} + +// NewREST returns a RESTStorage object that will work against leasecandidates. +func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) { + store := &genericregistry.Store{ + NewFunc: func() runtime.Object { return &coordinationapi.LeaseCandidate{} }, + NewListFunc: func() runtime.Object { return &coordinationapi.LeaseCandidateList{} }, + DefaultQualifiedResource: coordinationapi.Resource("leasecandidates"), + SingularQualifiedResource: coordinationapi.Resource("leasecandidate"), + + CreateStrategy: leasecandidate.Strategy, + UpdateStrategy: leasecandidate.Strategy, + DeleteStrategy: leasecandidate.Strategy, + + TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)}, + } + options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: leasecandidate.GetAttrs} + if err := store.CompleteWithOptions(options); err != nil { + return nil, err + } + + return &REST{store}, nil +} diff --git a/pkg/registry/coordination/leasecandidate/strategy.go b/pkg/registry/coordination/leasecandidate/strategy.go new file mode 100644 index 00000000000..2d5b07e2864 --- /dev/null +++ b/pkg/registry/coordination/leasecandidate/strategy.go @@ -0,0 +1,109 @@ +/* +Copyright 2024 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 leasecandidate + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/storage/names" + + "k8s.io/kubernetes/pkg/api/legacyscheme" + "k8s.io/kubernetes/pkg/apis/coordination" + "k8s.io/kubernetes/pkg/apis/coordination/validation" +) + +// LeaseCandidateStrategy implements verification logic for leasecandidates. +type LeaseCandidateStrategy struct { + runtime.ObjectTyper + names.NameGenerator +} + +// Strategy is the default logic that applies when creating and updating leasecandidate objects. +var Strategy = LeaseCandidateStrategy{legacyscheme.Scheme, names.SimpleNameGenerator} + +// NamespaceScoped returns true because all leasecandidate' need to be within a namespace. +func (LeaseCandidateStrategy) NamespaceScoped() bool { + return true +} + +// PrepareForCreate prepares leasecandidate for creation. +func (LeaseCandidateStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { +} + +// PrepareForUpdate clears fields that are not allowed to be set by end users on update. +func (LeaseCandidateStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) { +} + +// Validate validates a new leasecandidate. +func (LeaseCandidateStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { + leasecandidate := obj.(*coordination.LeaseCandidate) + return validation.ValidateLeaseCandidate(leasecandidate) +} + +// WarningsOnCreate returns warnings for the creation of the given object. +func (LeaseCandidateStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string { + return nil +} + +// Canonicalize normalizes the object after validation. +func (LeaseCandidateStrategy) Canonicalize(obj runtime.Object) { +} + +// AllowCreateOnUpdate is true for leasecandidate; this means you may create one with a PUT request. +func (LeaseCandidateStrategy) AllowCreateOnUpdate() bool { + return true +} + +// ValidateUpdate is the default update validation for an end user. +func (LeaseCandidateStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList { + return validation.ValidateLeaseCandidateUpdate(obj.(*coordination.LeaseCandidate), old.(*coordination.LeaseCandidate)) +} + +// WarningsOnUpdate returns warnings for the given update. +func (LeaseCandidateStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string { + return nil +} + +// AllowUnconditionalUpdate is the default update policy for leasecandidate objects. +func (LeaseCandidateStrategy) AllowUnconditionalUpdate() bool { + return false +} + +// GetAttrs returns labels and fields of a given object for filtering purposes. +func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) { + leasecandidate, ok := obj.(*coordination.LeaseCandidate) + if !ok { + return nil, nil, fmt.Errorf("not a leaseCandidate") + } + return labels.Set(leasecandidate.ObjectMeta.Labels), ToSelectableFields(leasecandidate), nil +} + +// ToSelectableFields returns a field set that represents the object +// TODO: fields are not labels, and the validation rules for them do not apply. +func ToSelectableFields(leasecandidate *coordination.LeaseCandidate) fields.Set { + objectMetaFieldsSet := generic.ObjectMetaFieldsSet(&leasecandidate.ObjectMeta, true) + specificFieldsSet := fields.Set{ + "spec.leaseName": string(leasecandidate.Spec.LeaseName), + } + return generic.MergeFieldsSets(objectMetaFieldsSet, specificFieldsSet) +} diff --git a/pkg/registry/coordination/rest/storage_coordination.go b/pkg/registry/coordination/rest/storage_coordination.go index 3cfe51d304e..f9bdf215bd8 100644 --- a/pkg/registry/coordination/rest/storage_coordination.go +++ b/pkg/registry/coordination/rest/storage_coordination.go @@ -18,6 +18,7 @@ package rest import ( coordinationv1 "k8s.io/api/coordination/v1" + coordinationv1alpha1 "k8s.io/api/coordination/v1alpha1" "k8s.io/apiserver/pkg/registry/generic" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" @@ -25,6 +26,7 @@ import ( "k8s.io/kubernetes/pkg/api/legacyscheme" "k8s.io/kubernetes/pkg/apis/coordination" leasestorage "k8s.io/kubernetes/pkg/registry/coordination/lease/storage" + leasecandidatestorage "k8s.io/kubernetes/pkg/registry/coordination/leasecandidate/storage" ) type RESTStorageProvider struct{} @@ -39,6 +41,13 @@ func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorag } else if len(storageMap) > 0 { apiGroupInfo.VersionedResourcesStorageMap[coordinationv1.SchemeGroupVersion.Version] = storageMap } + + if storageMap, err := p.v1alpha1Storage(apiResourceConfigSource, restOptionsGetter); err != nil { + return genericapiserver.APIGroupInfo{}, err + } else if len(storageMap) > 0 { + apiGroupInfo.VersionedResourcesStorageMap[coordinationv1alpha1.SchemeGroupVersion.Version] = storageMap + } + return apiGroupInfo, nil } @@ -56,6 +65,20 @@ func (p RESTStorageProvider) v1Storage(apiResourceConfigSource serverstorage.API return storage, nil } +func (p RESTStorageProvider) v1alpha1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (map[string]rest.Storage, error) { + storage := map[string]rest.Storage{} + + // identity + if resource := "leasecandidates"; apiResourceConfigSource.ResourceEnabled(coordinationv1alpha1.SchemeGroupVersion.WithResource(resource)) { + leaseCandidateStorage, err := leasecandidatestorage.NewREST(restOptionsGetter) + if err != nil { + return storage, err + } + storage[resource] = leaseCandidateStorage + } + return storage, nil +} + func (p RESTStorageProvider) GroupName() string { return coordination.GroupName } diff --git a/test/integration/etcd/data.go b/test/integration/etcd/data.go index 86dde1b8f2f..991e88bf029 100644 --- a/test/integration/etcd/data.go +++ b/test/integration/etcd/data.go @@ -172,6 +172,13 @@ func GetEtcdStorageDataForNamespace(namespace string) map[schema.GroupVersionRes }, // -- + // k8s.io/kubernetes/pkg/apis/coordination/v1alpha1 + gvr("coordination.k8s.io", "v1alpha1", "leasecandidates"): { + Stub: `{"metadata": {"name": "leasecandidatev1alpha1"}, "spec": {"leaseName": "lease"}}`, + ExpectedEtcdPath: "/registry/leasecandidates/" + namespace + "/leasecandidatev1alpha1", + }, + // -- + // k8s.io/kubernetes/pkg/apis/discovery/v1 gvr("discovery.k8s.io", "v1", "endpointslices"): { Stub: `{"metadata": {"name": "slicev1"}, "addressType": "IPv4", "protocol": "TCP", "ports": [], "endpoints": []}`,