From 6044f7e9a9675ff6e82e43ec78f80fe6248c4554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arda=20G=C3=BC=C3=A7l=C3=BC?= Date: Thu, 16 Jan 2025 10:45:28 +0300 Subject: [PATCH 1/6] Add configuration file assets and required functions --- PROJECT | 8 + api/config/v1alpha1/configuration_types.go | 130 +++++ api/config/v1alpha1/defaults.go | 90 ++++ api/config/v1alpha1/defaults_test.go | 275 +++++++++++ api/config/v1alpha1/groupversion_info.go | 45 ++ api/config/v1alpha1/zz_generated.deepcopy.go | 186 +++++++ api/config/v1alpha1/zz_generated.defaults.go | 34 ++ cmd/main.go | 114 +++-- cmd/main_test.go | 176 +++++++ config/default/manager_config_patch.yaml | 9 + config/manager/controller_manager_config.yaml | 4 + config/manager/kustomization.yaml | 9 + config/rbac/configuration_editor_role.yaml | 31 ++ config/rbac/configuration_viewer_role.yaml | 27 + .../samples/lws_v1alpha1_configuration.yaml | 12 + pkg/cert/cert.go | 13 +- pkg/config/config.go | 156 ++++++ pkg/config/config_test.go | 465 ++++++++++++++++++ pkg/config/validation.go | 55 +++ pkg/config/validation_test.go | 89 ++++ 20 files changed, 1896 insertions(+), 32 deletions(-) create mode 100644 api/config/v1alpha1/configuration_types.go create mode 100644 api/config/v1alpha1/defaults.go create mode 100644 api/config/v1alpha1/defaults_test.go create mode 100644 api/config/v1alpha1/groupversion_info.go create mode 100644 api/config/v1alpha1/zz_generated.deepcopy.go create mode 100644 api/config/v1alpha1/zz_generated.defaults.go create mode 100644 cmd/main_test.go create mode 100644 config/manager/controller_manager_config.yaml create mode 100644 config/rbac/configuration_editor_role.yaml create mode 100644 config/rbac/configuration_viewer_role.yaml create mode 100644 config/samples/lws_v1alpha1_configuration.yaml create mode 100644 pkg/config/config.go create mode 100644 pkg/config/config_test.go create mode 100644 pkg/config/validation.go create mode 100644 pkg/config/validation_test.go diff --git a/PROJECT b/PROJECT index eb1cccf9..cfcfb579 100644 --- a/PROJECT +++ b/PROJECT @@ -21,4 +21,12 @@ resources: defaulting: true validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + domain: lws.x-k8s.io + group: lws + kind: Configuration + path: sigs.k8s.io/lws/api/config/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/config/v1alpha1/configuration_types.go b/api/config/v1alpha1/configuration_types.go new file mode 100644 index 00000000..f6a9f0ad --- /dev/null +++ b/api/config/v1alpha1/configuration_types.go @@ -0,0 +1,130 @@ +/* +Copyright 2025 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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + configv1alpha1 "k8s.io/component-base/config/v1alpha1" +) + +// +k8s:defaulter-gen=true +// +kubebuilder:object:root=true + +// Configuration is the Schema for the configurations API +type Configuration struct { + metav1.TypeMeta `json:",inline"` + + // ControllerManager returns the configurations for controllers + ControllerManager `json:",inline"` + + // InternalCertManagerment is configuration for internalCertManagerment + InternalCertManagement *InternalCertManagement `json:"internalCertManagement,omitempty"` + + // ClientConnection is configuration of the client while connecting to API Server + ClientConnection *ClientConnection `json:"clientConnection,omitempty"` +} + +type ControllerManager struct { + // Webhook contains the controllers webhook configuration + // +optional + Webhook ControllerWebhook `json:"webhook,omitempty"` + + // LeaderElection is the LeaderElection config to be used when configuring + // the manager.Manager leader election + // +optional + LeaderElection *configv1alpha1.LeaderElectionConfiguration `json:"leaderElection,omitempty"` + + // Metrics contains the controller metrics configuration + // +optional + Metrics ControllerMetrics `json:"metrics,omitempty"` + + // Health contains the controller health configuration + // +optional + Health ControllerHealth `json:"health,omitempty"` +} + +// ControllerWebhook defines the webhook server for the controller. +type ControllerWebhook struct { + // Port is the port that the webhook server serves at. + // It is used to set webhook.Server.Port. + // +optional + Port *int `json:"port,omitempty"` + + // Host is the hostname that the webhook server binds to. + // It is used to set webhook.Server.Host. + // +optional + Host string `json:"host,omitempty"` + + // CertDir is the directory that contains the server key and certificate. + // if not set, webhook server would look up the server key and certificate in + // {TempDir}/k8s-webhook-server/serving-certs. The server key and certificate + // must be named tls.key and tls.crt, respectively. + // +optional + CertDir string `json:"certDir,omitempty"` +} + +// ControllerMetrics defines the metrics configs. +type ControllerMetrics struct { + // BindAddress is the TCP address that the controller should bind to + // for serving prometheus metrics. + // It can be set to "0" to disable the metrics serving. + // +optional + BindAddress string `json:"bindAddress,omitempty"` +} + +// ControllerHealth defines the health configs. +type ControllerHealth struct { + // HealthProbeBindAddress is the TCP address that the controller should bind to + // for serving health probes + // It can be set to "0" or "" to disable serving the health probe. + // +optional + HealthProbeBindAddress string `json:"healthProbeBindAddress,omitempty"` + + // ReadinessEndpointName, defaults to "readyz" + // +optional + ReadinessEndpointName string `json:"readinessEndpointName,omitempty"` + + // LivenessEndpointName, defaults to "healthz" + // +optional + LivenessEndpointName string `json:"livenessEndpointName,omitempty"` +} + +// InternalCertManagement defines internal certificate management configs +type InternalCertManagement struct { + // Enable controls whether to enable internal cert management or not. + // Defaults to true. If you want to use a third-party management, e.g. cert-manager, + // set it to false. See the user guide for more information. + Enable *bool `json:"enable,omitempty"` + + // WebhookServiceName is the name of the Service used as part of the DNSName. + // Defaults to lws-webhook-service. + WebhookServiceName *string `json:"webhookServiceName,omitempty"` + + // WebhookSecretName is the name of the Secret used to store CA and server certs. + // Defaults to lws-webhook-server-cert. + WebhookSecretName *string `json:"webhookSecretName,omitempty"` +} + +// ClientConnection defines the connection related fields while connecting to API Server +type ClientConnection struct { + // QPS controls the number of queries per second allowed for K8S api server + // connection. + QPS *float32 `json:"qps,omitempty"` + + // Burst allows extra queries to accumulate when a client is exceeding its rate. + Burst *int32 `json:"burst,omitempty"` +} diff --git a/api/config/v1alpha1/defaults.go b/api/config/v1alpha1/defaults.go new file mode 100644 index 00000000..1646bd35 --- /dev/null +++ b/api/config/v1alpha1/defaults.go @@ -0,0 +1,90 @@ +/* +Copyright 2025 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 v1alpha1 + +import ( + "time" + + configv1alpha1 "k8s.io/component-base/config/v1alpha1" + "k8s.io/utils/ptr" +) + +const ( + DefaultWebhookServiceName = "lws-webhook-service" + DefaultWebhookSecretName = "lws-webhook-server-cert" + DefaultWebhookPort = 9443 + DefaultHealthProbeBindAddress = ":8081" + DefaultMetricsBindAddress = ":8443" + DefaultLeaderElectionID = "b8b2488c.x-k8s.io" + DefaultLeaderElectionLeaseDuration = 15 * time.Second + DefaultLeaderElectionRenewDeadline = 10 * time.Second + DefaultLeaderElectionRetryPeriod = 2 * time.Second + DefaultResourceLock = "leases" + DefaultClientConnectionQPS float32 = 500 + DefaultClientConnectionBurst int32 = 500 +) + +// SetDefaults_Configuration sets default values for ComponentConfig. +// +//nolint:revive // format required by generated code for defaulting +func SetDefaults_Configuration(cfg *Configuration) { + if cfg.Webhook.Port == nil { + cfg.Webhook.Port = ptr.To(DefaultWebhookPort) + } + if len(cfg.Metrics.BindAddress) == 0 { + cfg.Metrics.BindAddress = DefaultMetricsBindAddress + } + if len(cfg.Health.HealthProbeBindAddress) == 0 { + cfg.Health.HealthProbeBindAddress = DefaultHealthProbeBindAddress + } + + if cfg.LeaderElection == nil { + cfg.LeaderElection = &configv1alpha1.LeaderElectionConfiguration{} + } + if len(cfg.LeaderElection.ResourceName) == 0 { + cfg.LeaderElection.ResourceName = DefaultLeaderElectionID + } + if len(cfg.LeaderElection.ResourceLock) == 0 { + cfg.LeaderElection.ResourceLock = DefaultResourceLock + } + // Use the default LeaderElectionConfiguration options + configv1alpha1.RecommendedDefaultLeaderElectionConfiguration(cfg.LeaderElection) + + if cfg.InternalCertManagement == nil { + cfg.InternalCertManagement = &InternalCertManagement{} + } + if cfg.InternalCertManagement.Enable == nil { + cfg.InternalCertManagement.Enable = ptr.To(true) + } + if *cfg.InternalCertManagement.Enable { + if cfg.InternalCertManagement.WebhookServiceName == nil { + cfg.InternalCertManagement.WebhookServiceName = ptr.To(DefaultWebhookServiceName) + } + if cfg.InternalCertManagement.WebhookSecretName == nil { + cfg.InternalCertManagement.WebhookSecretName = ptr.To(DefaultWebhookSecretName) + } + } + if cfg.ClientConnection == nil { + cfg.ClientConnection = &ClientConnection{} + } + if cfg.ClientConnection.QPS == nil { + cfg.ClientConnection.QPS = ptr.To(DefaultClientConnectionQPS) + } + if cfg.ClientConnection.Burst == nil { + cfg.ClientConnection.Burst = ptr.To(DefaultClientConnectionBurst) + } +} diff --git a/api/config/v1alpha1/defaults_test.go b/api/config/v1alpha1/defaults_test.go new file mode 100644 index 00000000..24e280fd --- /dev/null +++ b/api/config/v1alpha1/defaults_test.go @@ -0,0 +1,275 @@ +/* +Copyright 2025 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 v1alpha1 + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + configv1alpha1 "k8s.io/component-base/config/v1alpha1" + "k8s.io/utils/ptr" +) + +const ( + overwriteWebhookPort = 9444 + overwriteMetricBindAddress = ":38081" + overwriteHealthProbeBindAddress = ":38080" + overwriteLeaderElectionID = "foo.lws.x-k8s.io" +) + +func TestSetDefaults_Configuration(t *testing.T) { + defaultCtrlManagerConfigurationSpec := ControllerManager{ + LeaderElection: &configv1alpha1.LeaderElectionConfiguration{ + LeaderElect: ptr.To(true), + LeaseDuration: metav1.Duration{Duration: DefaultLeaderElectionLeaseDuration}, + RenewDeadline: metav1.Duration{Duration: DefaultLeaderElectionRenewDeadline}, + RetryPeriod: metav1.Duration{Duration: DefaultLeaderElectionRetryPeriod}, + ResourceLock: DefaultResourceLock, + ResourceName: DefaultLeaderElectionID, + }, + Webhook: ControllerWebhook{ + Port: ptr.To(DefaultWebhookPort), + }, + Metrics: ControllerMetrics{ + BindAddress: DefaultMetricsBindAddress, + }, + Health: ControllerHealth{ + HealthProbeBindAddress: DefaultHealthProbeBindAddress, + }, + } + defaultClientConnection := &ClientConnection{ + QPS: ptr.To(DefaultClientConnectionQPS), + Burst: ptr.To(DefaultClientConnectionBurst), + } + + testCases := map[string]struct { + original *Configuration + want *Configuration + }{ + "defaulting namespace": { + original: &Configuration{ + InternalCertManagement: &InternalCertManagement{ + Enable: ptr.To(false), + }, + }, + want: &Configuration{ + ControllerManager: defaultCtrlManagerConfigurationSpec, + InternalCertManagement: &InternalCertManagement{ + Enable: ptr.To(false), + }, + ClientConnection: defaultClientConnection, + }, + }, + "defaulting ControllerManager": { + original: &Configuration{ + ControllerManager: ControllerManager{ + LeaderElection: &configv1alpha1.LeaderElectionConfiguration{ + LeaderElect: ptr.To(true), + }, + }, + InternalCertManagement: &InternalCertManagement{ + Enable: ptr.To(false), + }, + }, + want: &Configuration{ + ControllerManager: ControllerManager{ + Webhook: ControllerWebhook{ + Port: ptr.To(DefaultWebhookPort), + }, + Metrics: ControllerMetrics{ + BindAddress: DefaultMetricsBindAddress, + }, + Health: ControllerHealth{ + HealthProbeBindAddress: DefaultHealthProbeBindAddress, + }, + LeaderElection: &configv1alpha1.LeaderElectionConfiguration{ + LeaderElect: ptr.To(true), + LeaseDuration: metav1.Duration{Duration: DefaultLeaderElectionLeaseDuration}, + RenewDeadline: metav1.Duration{Duration: DefaultLeaderElectionRenewDeadline}, + RetryPeriod: metav1.Duration{Duration: DefaultLeaderElectionRetryPeriod}, + ResourceLock: DefaultResourceLock, + ResourceName: DefaultLeaderElectionID, + }, + }, + InternalCertManagement: &InternalCertManagement{ + Enable: ptr.To(false), + }, + ClientConnection: defaultClientConnection, + }, + }, + "should not default ControllerManager": { + original: &Configuration{ + ControllerManager: ControllerManager{ + Webhook: ControllerWebhook{ + Port: ptr.To(overwriteWebhookPort), + }, + Metrics: ControllerMetrics{ + BindAddress: overwriteMetricBindAddress, + }, + Health: ControllerHealth{ + HealthProbeBindAddress: overwriteHealthProbeBindAddress, + }, + LeaderElection: &configv1alpha1.LeaderElectionConfiguration{ + LeaderElect: ptr.To(true), + LeaseDuration: metav1.Duration{Duration: DefaultLeaderElectionLeaseDuration}, + RenewDeadline: metav1.Duration{Duration: DefaultLeaderElectionRenewDeadline}, + RetryPeriod: metav1.Duration{Duration: DefaultLeaderElectionRetryPeriod}, + ResourceLock: DefaultResourceLock, + ResourceName: overwriteLeaderElectionID, + }, + }, + InternalCertManagement: &InternalCertManagement{ + Enable: ptr.To(false), + }, + }, + want: &Configuration{ + ControllerManager: ControllerManager{ + Webhook: ControllerWebhook{ + Port: ptr.To(overwriteWebhookPort), + }, + Metrics: ControllerMetrics{ + BindAddress: overwriteMetricBindAddress, + }, + Health: ControllerHealth{ + HealthProbeBindAddress: overwriteHealthProbeBindAddress, + }, + LeaderElection: &configv1alpha1.LeaderElectionConfiguration{ + LeaderElect: ptr.To(true), + LeaseDuration: metav1.Duration{Duration: DefaultLeaderElectionLeaseDuration}, + RenewDeadline: metav1.Duration{Duration: DefaultLeaderElectionRenewDeadline}, + RetryPeriod: metav1.Duration{Duration: DefaultLeaderElectionRetryPeriod}, + ResourceLock: DefaultResourceLock, + ResourceName: overwriteLeaderElectionID, + }, + }, + InternalCertManagement: &InternalCertManagement{ + Enable: ptr.To(false), + }, + ClientConnection: defaultClientConnection, + }, + }, + "should not set LeaderElectionID": { + original: &Configuration{ + ControllerManager: ControllerManager{ + LeaderElection: &configv1alpha1.LeaderElectionConfiguration{ + LeaderElect: ptr.To(false), + }, + }, + InternalCertManagement: &InternalCertManagement{ + Enable: ptr.To(false), + }, + }, + want: &Configuration{ + ControllerManager: ControllerManager{ + Webhook: ControllerWebhook{ + Port: ptr.To(DefaultWebhookPort), + }, + Metrics: ControllerMetrics{ + BindAddress: DefaultMetricsBindAddress, + }, + Health: ControllerHealth{ + HealthProbeBindAddress: DefaultHealthProbeBindAddress, + }, + LeaderElection: &configv1alpha1.LeaderElectionConfiguration{ + LeaderElect: ptr.To(false), + LeaseDuration: metav1.Duration{Duration: DefaultLeaderElectionLeaseDuration}, + RenewDeadline: metav1.Duration{Duration: DefaultLeaderElectionRenewDeadline}, + RetryPeriod: metav1.Duration{Duration: DefaultLeaderElectionRetryPeriod}, + ResourceLock: DefaultResourceLock, + ResourceName: DefaultLeaderElectionID, + }, + }, + InternalCertManagement: &InternalCertManagement{ + Enable: ptr.To(false), + }, + ClientConnection: defaultClientConnection, + }, + }, + "defaulting InternalCertManagement": { + original: &Configuration{}, + want: &Configuration{ + ControllerManager: defaultCtrlManagerConfigurationSpec, + InternalCertManagement: &InternalCertManagement{ + Enable: ptr.To(true), + WebhookServiceName: ptr.To(DefaultWebhookServiceName), + WebhookSecretName: ptr.To(DefaultWebhookSecretName), + }, + ClientConnection: defaultClientConnection, + }, + }, + "should not default InternalCertManagement": { + original: &Configuration{ + InternalCertManagement: &InternalCertManagement{ + Enable: ptr.To(false), + }, + }, + want: &Configuration{ + ControllerManager: defaultCtrlManagerConfigurationSpec, + InternalCertManagement: &InternalCertManagement{ + Enable: ptr.To(false), + }, + ClientConnection: defaultClientConnection, + }, + }, + "should not default values in custom ClientConnection": { + original: &Configuration{ + InternalCertManagement: &InternalCertManagement{ + Enable: ptr.To(false), + }, + ClientConnection: &ClientConnection{ + QPS: ptr.To[float32](123.0), + Burst: ptr.To[int32](456), + }, + }, + want: &Configuration{ + ControllerManager: defaultCtrlManagerConfigurationSpec, + InternalCertManagement: &InternalCertManagement{ + Enable: ptr.To(false), + }, + ClientConnection: &ClientConnection{ + QPS: ptr.To[float32](123.0), + Burst: ptr.To[int32](456), + }, + }, + }, + "should default empty custom ClientConnection": { + original: &Configuration{ + InternalCertManagement: &InternalCertManagement{ + Enable: ptr.To(false), + }, + ClientConnection: &ClientConnection{}, + }, + want: &Configuration{ + ControllerManager: defaultCtrlManagerConfigurationSpec, + InternalCertManagement: &InternalCertManagement{ + Enable: ptr.To(false), + }, + ClientConnection: defaultClientConnection, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + SetDefaults_Configuration(tc.original) + if diff := cmp.Diff(tc.want, tc.original); diff != "" { + t.Errorf("unexpected error (-want,+got):\n%s", diff) + } + }) + } +} diff --git a/api/config/v1alpha1/groupversion_info.go b/api/config/v1alpha1/groupversion_info.go new file mode 100644 index 00000000..7ae23669 --- /dev/null +++ b/api/config/v1alpha1/groupversion_info.go @@ -0,0 +1,45 @@ +/* +Copyright 2025 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 v1alpha1 contains API Schema definitions for the lws v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=lws.x-k8s.io +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "config.lws.x-k8s.io", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // localSchemeBuilder is used to register autogenerated conversion and defaults functions + // It is required by ./zz_generated.conversion.go and ./zz_generated.defaults.go + localSchemeBuilder = &SchemeBuilder.SchemeBuilder + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) + +func init() { + SchemeBuilder.Register(&Configuration{}) + localSchemeBuilder.Register(RegisterDefaults) +} diff --git a/api/config/v1alpha1/zz_generated.deepcopy.go b/api/config/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 00000000..de416bd5 --- /dev/null +++ b/api/config/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,186 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2025 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. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" + configv1alpha1 "k8s.io/component-base/config/v1alpha1" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClientConnection) DeepCopyInto(out *ClientConnection) { + *out = *in + if in.QPS != nil { + in, out := &in.QPS, &out.QPS + *out = new(float32) + **out = **in + } + if in.Burst != nil { + in, out := &in.Burst, &out.Burst + *out = new(int32) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientConnection. +func (in *ClientConnection) DeepCopy() *ClientConnection { + if in == nil { + return nil + } + out := new(ClientConnection) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Configuration) DeepCopyInto(out *Configuration) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ControllerManager.DeepCopyInto(&out.ControllerManager) + if in.InternalCertManagement != nil { + in, out := &in.InternalCertManagement, &out.InternalCertManagement + *out = new(InternalCertManagement) + (*in).DeepCopyInto(*out) + } + if in.ClientConnection != nil { + in, out := &in.ClientConnection, &out.ClientConnection + *out = new(ClientConnection) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Configuration. +func (in *Configuration) DeepCopy() *Configuration { + if in == nil { + return nil + } + out := new(Configuration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Configuration) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerHealth) DeepCopyInto(out *ControllerHealth) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerHealth. +func (in *ControllerHealth) DeepCopy() *ControllerHealth { + if in == nil { + return nil + } + out := new(ControllerHealth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerManager) DeepCopyInto(out *ControllerManager) { + *out = *in + in.Webhook.DeepCopyInto(&out.Webhook) + if in.LeaderElection != nil { + in, out := &in.LeaderElection, &out.LeaderElection + *out = new(configv1alpha1.LeaderElectionConfiguration) + (*in).DeepCopyInto(*out) + } + out.Metrics = in.Metrics + out.Health = in.Health +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerManager. +func (in *ControllerManager) DeepCopy() *ControllerManager { + if in == nil { + return nil + } + out := new(ControllerManager) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerMetrics) DeepCopyInto(out *ControllerMetrics) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerMetrics. +func (in *ControllerMetrics) DeepCopy() *ControllerMetrics { + if in == nil { + return nil + } + out := new(ControllerMetrics) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerWebhook) DeepCopyInto(out *ControllerWebhook) { + *out = *in + if in.Port != nil { + in, out := &in.Port, &out.Port + *out = new(int) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerWebhook. +func (in *ControllerWebhook) DeepCopy() *ControllerWebhook { + if in == nil { + return nil + } + out := new(ControllerWebhook) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InternalCertManagement) DeepCopyInto(out *InternalCertManagement) { + *out = *in + if in.Enable != nil { + in, out := &in.Enable, &out.Enable + *out = new(bool) + **out = **in + } + if in.WebhookServiceName != nil { + in, out := &in.WebhookServiceName, &out.WebhookServiceName + *out = new(string) + **out = **in + } + if in.WebhookSecretName != nil { + in, out := &in.WebhookSecretName, &out.WebhookSecretName + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalCertManagement. +func (in *InternalCertManagement) DeepCopy() *InternalCertManagement { + if in == nil { + return nil + } + out := new(InternalCertManagement) + in.DeepCopyInto(out) + return out +} diff --git a/api/config/v1alpha1/zz_generated.defaults.go b/api/config/v1alpha1/zz_generated.defaults.go new file mode 100644 index 00000000..5830742c --- /dev/null +++ b/api/config/v1alpha1/zz_generated.defaults.go @@ -0,0 +1,34 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +/* +Copyright 2025 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. +*/ +// Code generated by defaulter-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// RegisterDefaults adds defaulters functions to the given scheme. +// Public to allow building arbitrary schemes. +// All generated defaulters are covering - they call all nested defaulters. +func RegisterDefaults(scheme *runtime.Scheme) error { + scheme.AddTypeDefaultingFunc(&Configuration{}, func(obj interface{}) { SetObjectDefaults_Configuration(obj.(*Configuration)) }) + return nil +} + +func SetObjectDefaults_Configuration(in *Configuration) { + SetDefaults_Configuration(in) +} diff --git a/cmd/main.go b/cmd/main.go index 302f44d3..10e7a1aa 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -35,8 +35,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/metrics/filters" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + configapi "sigs.k8s.io/lws/api/config/v1alpha1" leaderworkersetv1 "sigs.k8s.io/lws/api/leaderworkerset/v1" "sigs.k8s.io/lws/pkg/cert" + "sigs.k8s.io/lws/pkg/config" "sigs.k8s.io/lws/pkg/controllers" "sigs.k8s.io/lws/pkg/utils" "sigs.k8s.io/lws/pkg/webhooks" @@ -46,12 +48,14 @@ import ( var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") + flagsSet = make(map[string]bool) ) func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(leaderworkersetv1.AddToScheme(scheme)) + utilruntime.Must(configapi.AddToScheme(scheme)) //+kubebuilder:scaffold:scheme } @@ -69,6 +73,7 @@ func main() { leaderElectRetryPeriod time.Duration leaderElectResourceLock string leaderElectionID string + configFile string ) flag.StringVar(&metricsAddr, "metrics-bind-address", ":8443", "The address the metric endpoint binds to.") @@ -93,6 +98,10 @@ func main() { "'endpoints', 'configmaps', 'leases', 'endpointsleases' and 'configmapsleases'") flag.StringVar(&leaderElectionID, "leader-elect-resource-name", "b8b2488c.x-k8s.io", "The name of resource object that is used for locking during leader election. ") + flag.StringVar(&configFile, "config", "", + "The controller will load its initial configuration from this file. "+ + "Command-line flags will override any configurations set in this file. "+ + "Omit this flag to use the default configuration values.") opts := zap.Options{ Development: true, @@ -100,11 +109,29 @@ func main() { opts.BindFlags(flag.CommandLine) flag.Parse() + flag.Visit(func(f *flag.Flag) { + flagsSet[f.Name] = true + }) + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + options, cfg, err := apply(configFile, probeAddr, enableLeaderElection, leaderElectLeaseDuration, leaderElectRenewDeadline, leaderElectRetryPeriod, leaderElectResourceLock, leaderElectionID) + if err != nil { + setupLog.Error(err, "unable to load the configuration") + os.Exit(1) + } + kubeConfig := ctrl.GetConfigOrDie() - kubeConfig.QPS = float32(qps) - kubeConfig.Burst = burst + + kubeConfig.QPS = *cfg.ClientConnection.QPS + if flagsSet["kube-api-qps"] { + kubeConfig.QPS = float32(qps) + } + kubeConfig.Burst = int(*cfg.ClientConnection.Burst) + if flagsSet["kube-api-burst"] { + kubeConfig.Burst = burst + } + namespace := utils.GetOperatorNamespace() // Disabling http/2 to prevent being vulnerable to the HTTP/2 Stream Cancellation and @@ -116,6 +143,10 @@ func main() { c.NextProtos = []string{"http/1.1"} } + if !flagsSet["metrics-bind-address"] { + metricsAddr = cfg.Metrics.BindAddress + } + // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. // More info: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server @@ -127,40 +158,23 @@ func main() { TLSOpts: []func(*tls.Config){disableHTTP2}, } - mgr, err := ctrl.NewManager(kubeConfig, ctrl.Options{ - Scheme: scheme, - Metrics: metricsServerOptions, - HealthProbeBindAddress: probeAddr, - LeaderElection: enableLeaderElection, - LeaderElectionID: leaderElectionID, - LeaderElectionResourceLock: leaderElectResourceLock, - LeaderElectionNamespace: namespace, // Using namespace variable here - LeaseDuration: &leaderElectLeaseDuration, - RenewDeadline: &leaderElectRenewDeadline, - RetryPeriod: &leaderElectRetryPeriod, - // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily - // when the Manager ends. This requires the binary to immediately end when the - // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly - // speeds up voluntary leader transitions as the new leader don't have to wait - // LeaseDuration time first. - // - // In the default scaffold provided, the program ends immediately after - // the manager stops, so would be fine to enable this option. However, - // if you are doing or is intended to do any operation such as perform cleanups - // after the manager stops then its usage might be unsafe. - // LeaderElectionReleaseOnCancel: true, - }) + options.Metrics = metricsServerOptions + options.LeaderElectionNamespace = namespace + mgr, err := ctrl.NewManager(kubeConfig, options) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } certsReady := make(chan struct{}) - - if err = cert.CertsManager(mgr, namespace, certsReady); err != nil { - setupLog.Error(err, "unable to setup cert rotation") - os.Exit(1) + if cfg.InternalCertManagement != nil && *cfg.InternalCertManagement.Enable { + if err = cert.CertsManager(mgr, namespace, *cfg.InternalCertManagement.WebhookServiceName, *cfg.InternalCertManagement.WebhookSecretName, certsReady); err != nil { + setupLog.Error(err, "unable to setup cert rotation") + os.Exit(1) + } + } else { + close(certsReady) } if err := controllers.SetupIndexes(mgr.GetFieldIndexer()); err != nil { @@ -226,3 +240,45 @@ func setupHealthzAndReadyzCheck(mgr ctrl.Manager) { os.Exit(1) } } + +func apply(configFile string, + probeAddr string, + enableLeaderElection bool, + leaderElectLeaseDuration time.Duration, + leaderElectRenewDeadline time.Duration, + leaderElectRetryPeriod time.Duration, + leaderElectResourceLock, leaderElectionID string) (ctrl.Options, configapi.Configuration, error) { + options, cfg, err := config.Load(scheme, configFile) + if err != nil { + return options, cfg, err + } + cfgStr, err := config.Encode(scheme, &cfg) + if err != nil { + return options, cfg, err + } + + if flagsSet["health-probe-bind-address"] { + options.HealthProbeBindAddress = probeAddr + } + if flagsSet["leader-elect"] { + options.LeaderElection = enableLeaderElection + } + if flagsSet["leader-elect-lease-duration"] { + options.LeaseDuration = &leaderElectLeaseDuration + } + if flagsSet["leader-elect-renew-deadline"] { + options.RenewDeadline = &leaderElectRenewDeadline + } + if flagsSet["leader-elect-retry-period"] { + options.RetryPeriod = &leaderElectRetryPeriod + } + if flagsSet["leader-elect-resource-lock"] { + options.LeaderElectionResourceLock = leaderElectResourceLock + } + if flagsSet["leader-elect-resource-name"] { + options.LeaderElectionID = leaderElectionID + } + + setupLog.Info("Successfully loaded configuration", "config", cfgStr) + return options, cfg, nil +} diff --git a/cmd/main_test.go b/cmd/main_test.go new file mode 100644 index 00000000..ace9f6ba --- /dev/null +++ b/cmd/main_test.go @@ -0,0 +1,176 @@ +/* +Copyright 2025. + +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 main + +import ( + "net" + "os" + "path/filepath" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +func TestApply(t *testing.T) { + tmpDir := t.TempDir() + + testConfig := filepath.Join(tmpDir, "test_config.yaml") + if err := os.WriteFile(testConfig, []byte(` +apiVersion: config.lws.x-k8s.io/v1alpha1 +kind: Configuration +health: + healthProbeBindAddress: :8081 +leaderElection: + leaderElect: true + resourceName: test + leaseDuration: 5m + renewDeadline: 5m + retryPeriod: 5m + resourceLock: test +webhook: + port: 9443 +internalCertManagement: + enable: true + webhookServiceName: lws-tenant-a-webhook-service + webhookSecretName: lws-tenant-a-webhook-server-cert +`), os.FileMode(0600)); err != nil { + t.Fatal(err) + } + + ctrlOptsCmpOpts := []cmp.Option{ + cmpopts.IgnoreUnexported(ctrl.Options{}), + cmpopts.IgnoreUnexported(webhook.DefaultServer{}), + cmpopts.IgnoreUnexported(ctrlcache.Options{}), + cmpopts.IgnoreUnexported(net.ListenConfig{}), + cmpopts.IgnoreFields(ctrl.Options{}, "Scheme", "Logger", "Metrics", "WebhookServer"), + } + + testCases := []struct { + name string + flagtrack map[string]bool + configFile string + probeAddr string + enableLeaderElection bool + leaderElectLeaseDuration time.Duration + leaderElectRenewDeadline time.Duration + leaderElectRetryPeriod time.Duration + leaderElectResourceLock string + leaderElectionID string + expectedOpts ctrl.Options + }{ + { + name: "flags overwrite", + configFile: testConfig, + flagtrack: map[string]bool{ + "health-probe-bind-address": true, + "leader-elect": true, + "leader-elect-lease-duration": true, + "leader-elect-renew-deadline": true, + "leader-elect-retry-period": true, + "leader-elect-resource-lock": true, + "leader-elect-resource-name": true, + }, + probeAddr: ":9443", + enableLeaderElection: false, + leaderElectLeaseDuration: 1 * time.Minute, + leaderElectRenewDeadline: 1 * time.Minute, + leaderElectRetryPeriod: 1 * time.Minute, + leaderElectResourceLock: "changed", + leaderElectionID: "changed", + expectedOpts: ctrl.Options{ + LeaderElection: false, + LeaderElectionResourceLock: "changed", + LeaderElectionID: "changed", + LeaseDuration: ptr.To(1 * time.Minute), + RenewDeadline: ptr.To(1 * time.Minute), + RetryPeriod: ptr.To(1 * time.Minute), + HealthProbeBindAddress: ":9443", + }, + }, + { + name: "no flag overwrite", + configFile: testConfig, + flagtrack: map[string]bool{}, + probeAddr: ":9443", + enableLeaderElection: false, + leaderElectLeaseDuration: 1 * time.Minute, + leaderElectRenewDeadline: 1 * time.Minute, + leaderElectRetryPeriod: 1 * time.Minute, + leaderElectResourceLock: "changed", + leaderElectionID: "changed", + expectedOpts: ctrl.Options{ + LeaderElection: true, + LeaderElectionResourceLock: "test", + LeaderElectionID: "test", + LeaseDuration: ptr.To(5 * time.Minute), + RenewDeadline: ptr.To(5 * time.Minute), + RetryPeriod: ptr.To(5 * time.Minute), + HealthProbeBindAddress: ":8081", + }, + }, + { + name: "partial flag overwrite", + configFile: testConfig, + flagtrack: map[string]bool{ + "health-probe-bind-address": true, + "leader-elect": true, + }, + probeAddr: ":9443", + enableLeaderElection: false, + leaderElectLeaseDuration: 1 * time.Minute, + leaderElectRenewDeadline: 1 * time.Minute, + leaderElectRetryPeriod: 1 * time.Minute, + leaderElectResourceLock: "changed", + leaderElectionID: "changed", + expectedOpts: ctrl.Options{ + LeaderElection: false, + LeaderElectionResourceLock: "test", + LeaderElectionID: "test", + LeaseDuration: ptr.To(5 * time.Minute), + RenewDeadline: ptr.To(5 * time.Minute), + RetryPeriod: ptr.To(5 * time.Minute), + HealthProbeBindAddress: ":9443", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + flagsSet = tc.flagtrack + opts, _, err := apply(tc.configFile, + tc.probeAddr, + tc.enableLeaderElection, + tc.leaderElectLeaseDuration, + tc.leaderElectRenewDeadline, + tc.leaderElectRetryPeriod, + tc.leaderElectResourceLock, + tc.leaderElectionID) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if diff := cmp.Diff(tc.expectedOpts, opts, ctrlOptsCmpOpts...); diff != "" { + t.Errorf("Unexpected options (-want +got):\n%s", diff) + } + }) + } +} diff --git a/config/default/manager_config_patch.yaml b/config/default/manager_config_patch.yaml index 2936c961..6b5320ae 100644 --- a/config/default/manager_config_patch.yaml +++ b/config/default/manager_config_patch.yaml @@ -9,4 +9,13 @@ spec: containers: - name: manager args: + - "--config=/controller_manager_config.yaml" - "--zap-log-level=2" + volumeMounts: + - name: manager-config + mountPath: /controller_manager_config.yaml + subPath: controller_manager_config.yaml + volumes: + - name: manager-config + configMap: + name: manager-config diff --git a/config/manager/controller_manager_config.yaml b/config/manager/controller_manager_config.yaml new file mode 100644 index 00000000..65c590e9 --- /dev/null +++ b/config/manager/controller_manager_config.yaml @@ -0,0 +1,4 @@ +apiVersion: config.lws.x-k8s.io/v1alpha1 +kind: Configuration +leaderElection: + leaderElect: true diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index e6ccc001..9a508fde 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,5 +1,14 @@ resources: - manager.yaml + +generatorOptions: + disableNameSuffixHash: true + +configMapGenerator: + - files: + - controller_manager_config.yaml + name: manager-config + apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: diff --git a/config/rbac/configuration_editor_role.yaml b/config/rbac/configuration_editor_role.yaml new file mode 100644 index 00000000..bc397fee --- /dev/null +++ b/config/rbac/configuration_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit configurations. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: configuration-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: lws + app.kubernetes.io/part-of: lws + app.kubernetes.io/managed-by: kustomize + name: configuration-editor-role +rules: +- apiGroups: + - lws.x-k8s.io + resources: + - configurations + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - lws.x-k8s.io + resources: + - configurations/status + verbs: + - get diff --git a/config/rbac/configuration_viewer_role.yaml b/config/rbac/configuration_viewer_role.yaml new file mode 100644 index 00000000..eae35fb8 --- /dev/null +++ b/config/rbac/configuration_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view configurations. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: configuration-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: lws + app.kubernetes.io/part-of: lws + app.kubernetes.io/managed-by: kustomize + name: configuration-viewer-role +rules: +- apiGroups: + - lws.x-k8s.io + resources: + - configurations + verbs: + - get + - list + - watch +- apiGroups: + - lws.x-k8s.io + resources: + - configurations/status + verbs: + - get diff --git a/config/samples/lws_v1alpha1_configuration.yaml b/config/samples/lws_v1alpha1_configuration.yaml new file mode 100644 index 00000000..1d7e3cfc --- /dev/null +++ b/config/samples/lws_v1alpha1_configuration.yaml @@ -0,0 +1,12 @@ +apiVersion: lws.x-k8s.io/v1alpha1 +kind: Configuration +metadata: + labels: + app.kubernetes.io/name: configuration + app.kubernetes.io/instance: configuration-sample + app.kubernetes.io/part-of: lws + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: lws + name: configuration-sample +spec: + # TODO(user): Add fields here diff --git a/pkg/cert/cert.go b/pkg/cert/cert.go index 39dd71cf..8de62e80 100644 --- a/pkg/cert/cert.go +++ b/pkg/cert/cert.go @@ -36,14 +36,21 @@ const ( //+kubebuilder:rbac:groups="admissionregistration.k8s.io",resources=validatingwebhookconfigurations,verbs=get;list;watch;update // CertsManager creates certs for webhooks. -func CertsManager(mgr ctrl.Manager, namespace string, setupFinish chan struct{}) error { +func CertsManager(mgr ctrl.Manager, namespace string, configServiceName string, configSecretName string, setupFinish chan struct{}) error { + if configServiceName == "" { + configServiceName = serviceName + } + if configSecretName == "" { + configSecretName = secretName + } + // dnsName is the format of ..svc - var dnsName = fmt.Sprintf("%s.%s.svc", serviceName, namespace) + var dnsName = fmt.Sprintf("%s.%s.svc", configServiceName, namespace) return cert.AddRotator(mgr, &cert.CertRotator{ SecretKey: types.NamespacedName{ Namespace: namespace, - Name: secretName, + Name: configSecretName, }, CertDir: certDir, CAName: caName, diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 00000000..7b74dbc0 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,156 @@ +/* +Copyright 2025. + +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 config + +import ( + "bytes" + "fmt" + "os" + + "k8s.io/apimachinery/pkg/api/equality" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + configapi "sigs.k8s.io/lws/api/config/v1alpha1" +) + +func fromFile(path string, scheme *runtime.Scheme, cfg *configapi.Configuration) error { + content, err := os.ReadFile(path) + if err != nil { + return err + } + + codecs := serializer.NewCodecFactory(scheme, serializer.EnableStrict) + + // Regardless of if the bytes are of any external version, + // it will be read successfully and converted into the internal version + return runtime.DecodeInto(codecs.UniversalDecoder(), content, cfg) +} + +// addTo applies the configuration from cfg to the controller-runtime Options o. +// It only sets values in o if they are not already set and are present in cfg. +func addTo(o *ctrl.Options, cfg *configapi.Configuration) { + addLeaderElectionTo(o, cfg) + + if o.Metrics.BindAddress == "" && cfg.Metrics.BindAddress != "" { + o.Metrics.BindAddress = cfg.Metrics.BindAddress + } + + if o.HealthProbeBindAddress == "" && cfg.Health.HealthProbeBindAddress != "" { + o.HealthProbeBindAddress = cfg.Health.HealthProbeBindAddress + } + + if o.ReadinessEndpointName == "" && cfg.Health.ReadinessEndpointName != "" { + o.ReadinessEndpointName = cfg.Health.ReadinessEndpointName + } + + if o.LivenessEndpointName == "" && cfg.Health.LivenessEndpointName != "" { + o.LivenessEndpointName = cfg.Health.LivenessEndpointName + } + + if o.WebhookServer == nil && cfg.Webhook.Port != nil { + wo := webhook.Options{} + if cfg.Webhook.Port != nil { + wo.Port = *cfg.Webhook.Port + } + if cfg.Webhook.Host != "" { + wo.Host = cfg.Webhook.Host + } + if cfg.Webhook.CertDir != "" { + wo.CertDir = cfg.Webhook.CertDir + } + o.WebhookServer = webhook.NewServer(wo) + } +} + +func addLeaderElectionTo(o *ctrl.Options, cfg *configapi.Configuration) { + if cfg.LeaderElection == nil { + // The source does not have any configuration; noop + return + } + + if !o.LeaderElection && cfg.LeaderElection.LeaderElect != nil { + o.LeaderElection = *cfg.LeaderElection.LeaderElect + } + + if o.LeaderElectionResourceLock == "" && cfg.LeaderElection.ResourceLock != "" { + o.LeaderElectionResourceLock = cfg.LeaderElection.ResourceLock + } + + if o.LeaderElectionNamespace == "" && cfg.LeaderElection.ResourceNamespace != "" { + o.LeaderElectionNamespace = cfg.LeaderElection.ResourceNamespace + } + + if o.LeaderElectionID == "" && cfg.LeaderElection.ResourceName != "" { + o.LeaderElectionID = cfg.LeaderElection.ResourceName + } + + if o.LeaseDuration == nil && !equality.Semantic.DeepEqual(cfg.LeaderElection.LeaseDuration, metav1.Duration{}) { + o.LeaseDuration = &cfg.LeaderElection.LeaseDuration.Duration + } + + if o.RenewDeadline == nil && !equality.Semantic.DeepEqual(cfg.LeaderElection.RenewDeadline, metav1.Duration{}) { + o.RenewDeadline = &cfg.LeaderElection.RenewDeadline.Duration + } + + if o.RetryPeriod == nil && !equality.Semantic.DeepEqual(cfg.LeaderElection.RetryPeriod, metav1.Duration{}) { + o.RetryPeriod = &cfg.LeaderElection.RetryPeriod.Duration + } +} + +func Encode(scheme *runtime.Scheme, cfg *configapi.Configuration) (string, error) { + codecs := serializer.NewCodecFactory(scheme) + const mediaType = runtime.ContentTypeYAML + info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), mediaType) + if !ok { + return "", fmt.Errorf("unable to locate encoder -- %q is not a supported media type", mediaType) + } + + encoder := codecs.EncoderForVersion(info.Serializer, configapi.GroupVersion) + buf := new(bytes.Buffer) + if err := encoder.Encode(cfg, buf); err != nil { + return "", err + } + return buf.String(), nil +} + +// Load returns a set of controller options and configuration from the given file, if the config file path is empty +// it used the default configapi values. +func Load(scheme *runtime.Scheme, configFile string) (ctrl.Options, configapi.Configuration, error) { + var err error + options := ctrl.Options{ + Scheme: scheme, + } + + cfg := configapi.Configuration{} + if configFile == "" { + scheme.Default(&cfg) + } else { + err := fromFile(configFile, scheme, &cfg) + if err != nil { + return options, cfg, err + } + } + if err := validate(&cfg).ToAggregate(); err != nil { + return options, cfg, err + } + addTo(&options, &cfg) + return options, cfg, err +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 00000000..c9c66197 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,465 @@ +/* +Copyright 2025 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 config + +import ( + "errors" + "io/fs" + "net" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/client-go/tools/leaderelection/resourcelock" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + ctrlcache "sigs.k8s.io/controller-runtime/pkg/cache" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" + + configapi "sigs.k8s.io/lws/api/config/v1alpha1" +) + +func TestLoad(t *testing.T) { + testScheme := runtime.NewScheme() + err := configapi.AddToScheme(testScheme) + if err != nil { + t.Fatal(err) + } + + tmpDir := t.TempDir() + + ctrlManagerConfigSpecOverWriteConfig := filepath.Join(tmpDir, "ctrl-manager-config-spec-overwrite.yaml") + if err := os.WriteFile(ctrlManagerConfigSpecOverWriteConfig, []byte(` +apiVersion: config.lws.x-k8s.io/v1alpha1 +kind: Configuration +health: + healthProbeBindAddress: :38081 +metrics: + bindAddress: :38080 +leaderElection: + leaderElect: true + resourceName: test-id +webhook: + port: 9444 +`), os.FileMode(0600)); err != nil { + t.Fatal(err) + } + + certOverWriteConfig := filepath.Join(tmpDir, "cert-overwrite.yaml") + if err := os.WriteFile(certOverWriteConfig, []byte(` +apiVersion: config.lws.x-k8s.io/v1alpha1 +kind: Configuration +health: + healthProbeBindAddress: :8081 +metrics: + bindAddress: :8443 +leaderElection: + leaderElect: true + resourceName: b8b2488c.x-k8s.io +webhook: + port: 9443 +internalCertManagement: + enable: true + webhookServiceName: lws-tenant-a-webhook-service + webhookSecretName: lws-tenant-a-webhook-server-cert +`), os.FileMode(0600)); err != nil { + t.Fatal(err) + } + + disableCertOverWriteConfig := filepath.Join(tmpDir, "disable-cert-overwrite.yaml") + if err := os.WriteFile(disableCertOverWriteConfig, []byte(` +apiVersion: config.lws.x-k8s.io/v1alpha1 +kind: Configuration +health: + healthProbeBindAddress: :8081 +metrics: + bindAddress: :8443 +leaderElection: + leaderElect: true + resourceName: b8b2488c.x-k8s.io +webhook: + port: 9443 +internalCertManagement: + enable: false +`), os.FileMode(0600)); err != nil { + t.Fatal(err) + } + + leaderElectionDisabledConfig := filepath.Join(tmpDir, "leaderElection-disabled.yaml") + if err := os.WriteFile(leaderElectionDisabledConfig, []byte(` +apiVersion: config.lws.x-k8s.io/v1alpha1 +kind: Configuration +health: + healthProbeBindAddress: :8081 +metrics: + bindAddress: :8443 +leaderElection: + leaderElect: false +webhook: + port: 9443 +`), os.FileMode(0600)); err != nil { + t.Fatal(err) + } + + clientConnectionConfig := filepath.Join(tmpDir, "clientConnection.yaml") + if err := os.WriteFile(clientConnectionConfig, []byte(` +apiVersion: config.lws.x-k8s.io/v1alpha1 +kind: Configuration +health: + healthProbeBindAddress: :8081 +metrics: + bindAddress: :8443 +leaderElection: + leaderElect: true + resourceName: b8b2488c.x-k8s.io +webhook: + port: 9443 +clientConnection: + qps: 50 + burst: 100 +`), os.FileMode(0600)); err != nil { + t.Fatal(err) + } + + invalidConfig := filepath.Join(tmpDir, "invalid-config.yaml") + if err := os.WriteFile(invalidConfig, []byte(` +apiVersion: config.lws.x-k8s.io/v1alpha1 +kind: Configuration +invalidField: invalidValue +health: + healthProbeBindAddress: :8081 +metrics: + bindAddress: :8443 +leaderElection: + leaderElect: true + resourceName: b8b2488c.x-k8s.io +webhook: + port: 9443 +`), os.FileMode(0600)); err != nil { + t.Fatal(err) + } + + defaultControlOptions := ctrl.Options{ + HealthProbeBindAddress: configapi.DefaultHealthProbeBindAddress, + Metrics: metricsserver.Options{ + BindAddress: configapi.DefaultMetricsBindAddress, + }, + LeaderElection: true, + LeaderElectionID: configapi.DefaultLeaderElectionID, + LeaderElectionResourceLock: resourcelock.LeasesResourceLock, + LeaseDuration: ptr.To(configapi.DefaultLeaderElectionLeaseDuration), + RenewDeadline: ptr.To(configapi.DefaultLeaderElectionRenewDeadline), + RetryPeriod: ptr.To(configapi.DefaultLeaderElectionRetryPeriod), + WebhookServer: &webhook.DefaultServer{ + Options: webhook.Options{ + Port: configapi.DefaultWebhookPort, + }, + }, + } + + enableDefaultInternalCertManagement := &configapi.InternalCertManagement{ + Enable: ptr.To(true), + WebhookServiceName: ptr.To(configapi.DefaultWebhookServiceName), + WebhookSecretName: ptr.To(configapi.DefaultWebhookSecretName), + } + + ctrlOptsCmpOpts := []cmp.Option{ + cmpopts.IgnoreUnexported(ctrl.Options{}), + cmpopts.IgnoreUnexported(webhook.DefaultServer{}), + cmpopts.IgnoreUnexported(ctrlcache.Options{}), + cmpopts.IgnoreUnexported(net.ListenConfig{}), + cmpopts.IgnoreFields(ctrl.Options{}, "Scheme", "Logger"), + } + + // Ignore the controller manager section since it's side effect is checked against + // the content of the resulting options + configCmpOpts := []cmp.Option{ + cmpopts.IgnoreFields(configapi.Configuration{}, "ControllerManager"), + } + + defaultClientConnection := &configapi.ClientConnection{ + QPS: ptr.To[float32](configapi.DefaultClientConnectionQPS), + Burst: ptr.To[int32](configapi.DefaultClientConnectionBurst), + } + + testcases := []struct { + name string + configFile string + wantConfiguration configapi.Configuration + wantOptions ctrl.Options + wantError error + }{ + { + name: "default config", + configFile: "", + wantConfiguration: configapi.Configuration{ + InternalCertManagement: enableDefaultInternalCertManagement, + ClientConnection: defaultClientConnection, + }, + wantOptions: ctrl.Options{ + HealthProbeBindAddress: configapi.DefaultHealthProbeBindAddress, + Metrics: metricsserver.Options{ + BindAddress: configapi.DefaultMetricsBindAddress, + }, + LeaderElection: true, + LeaderElectionID: configapi.DefaultLeaderElectionID, + LeaderElectionResourceLock: resourcelock.LeasesResourceLock, + LeaseDuration: ptr.To(configapi.DefaultLeaderElectionLeaseDuration), + RenewDeadline: ptr.To(configapi.DefaultLeaderElectionRenewDeadline), + RetryPeriod: ptr.To(configapi.DefaultLeaderElectionRetryPeriod), + WebhookServer: &webhook.DefaultServer{ + Options: webhook.Options{ + Port: configapi.DefaultWebhookPort, + }, + }, + }, + }, + { + name: "bad path", + configFile: ".", + wantError: &fs.PathError{ + Op: "read", + Path: ".", + Err: errors.New("is a directory"), + }, + }, + { + name: "ControllerManagerConfigurationSpec overwrite config", + configFile: ctrlManagerConfigSpecOverWriteConfig, + wantConfiguration: configapi.Configuration{ + TypeMeta: metav1.TypeMeta{ + APIVersion: configapi.GroupVersion.String(), + Kind: "Configuration", + }, + InternalCertManagement: enableDefaultInternalCertManagement, + ClientConnection: defaultClientConnection, + }, + wantOptions: ctrl.Options{ + HealthProbeBindAddress: ":38081", + Metrics: metricsserver.Options{ + BindAddress: ":38080", + }, + LeaderElection: true, + LeaderElectionID: "test-id", + LeaderElectionResourceLock: resourcelock.LeasesResourceLock, + LeaseDuration: ptr.To(configapi.DefaultLeaderElectionLeaseDuration), + RenewDeadline: ptr.To(configapi.DefaultLeaderElectionRenewDeadline), + RetryPeriod: ptr.To(configapi.DefaultLeaderElectionRetryPeriod), + WebhookServer: &webhook.DefaultServer{ + Options: webhook.Options{ + Port: 9444, + }, + }, + }, + }, + { + name: "cert options overwrite config", + configFile: certOverWriteConfig, + wantConfiguration: configapi.Configuration{ + TypeMeta: metav1.TypeMeta{ + APIVersion: configapi.GroupVersion.String(), + Kind: "Configuration", + }, + InternalCertManagement: &configapi.InternalCertManagement{ + Enable: ptr.To(true), + WebhookServiceName: ptr.To("lws-tenant-a-webhook-service"), + WebhookSecretName: ptr.To("lws-tenant-a-webhook-server-cert"), + }, + ClientConnection: defaultClientConnection, + }, + wantOptions: defaultControlOptions, + }, + { + name: "disable cert overwrite config", + configFile: disableCertOverWriteConfig, + wantConfiguration: configapi.Configuration{ + TypeMeta: metav1.TypeMeta{ + APIVersion: configapi.GroupVersion.String(), + Kind: "Configuration", + }, + InternalCertManagement: &configapi.InternalCertManagement{ + Enable: ptr.To(false), + }, + ClientConnection: defaultClientConnection, + }, + wantOptions: defaultControlOptions, + }, + { + name: "leaderElection disabled config", + configFile: leaderElectionDisabledConfig, + wantConfiguration: configapi.Configuration{ + TypeMeta: metav1.TypeMeta{ + APIVersion: configapi.GroupVersion.String(), + Kind: "Configuration", + }, + InternalCertManagement: enableDefaultInternalCertManagement, + ClientConnection: defaultClientConnection, + }, + wantOptions: ctrl.Options{ + HealthProbeBindAddress: configapi.DefaultHealthProbeBindAddress, + Metrics: metricsserver.Options{ + BindAddress: configapi.DefaultMetricsBindAddress, + }, + LeaderElectionID: configapi.DefaultLeaderElectionID, + LeaderElectionResourceLock: resourcelock.LeasesResourceLock, + LeaseDuration: ptr.To(configapi.DefaultLeaderElectionLeaseDuration), + RenewDeadline: ptr.To(configapi.DefaultLeaderElectionRenewDeadline), + RetryPeriod: ptr.To(configapi.DefaultLeaderElectionRetryPeriod), + LeaderElection: false, + WebhookServer: &webhook.DefaultServer{ + Options: webhook.Options{ + Port: configapi.DefaultWebhookPort, + }, + }, + }, + }, + { + name: "clientConnection config", + configFile: clientConnectionConfig, + wantConfiguration: configapi.Configuration{ + TypeMeta: metav1.TypeMeta{ + APIVersion: configapi.GroupVersion.String(), + Kind: "Configuration", + }, + InternalCertManagement: enableDefaultInternalCertManagement, + ClientConnection: &configapi.ClientConnection{ + QPS: ptr.To[float32](50), + Burst: ptr.To[int32](100), + }, + }, + wantOptions: defaultControlOptions, + }, + { + name: "invalid config", + configFile: invalidConfig, + wantError: runtime.NewStrictDecodingError([]error{ + errors.New("unknown field \"invalidField\""), + }), + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + options, cfg, err := Load(testScheme, tc.configFile) + if tc.wantError == nil { + if err != nil { + t.Errorf("Unexpected error:%s", err) + } + if diff := cmp.Diff(tc.wantConfiguration, cfg, configCmpOpts...); diff != "" { + t.Errorf("Unexpected config (-want +got):\n%s", diff) + } + if diff := cmp.Diff(tc.wantOptions, options, ctrlOptsCmpOpts...); diff != "" { + t.Errorf("Unexpected options (-want +got):\n%s", diff) + } + } else { + if diff := cmp.Diff(tc.wantError.Error(), err.Error()); diff != "" { + t.Errorf("Unexpected error (-want +got):\n%s", diff) + } + } + }) + } +} + +func TestEncode(t *testing.T) { + testScheme := runtime.NewScheme() + err := configapi.AddToScheme(testScheme) + if err != nil { + t.Fatal(err) + } + + defaultConfig := &configapi.Configuration{} + testScheme.Default(defaultConfig) + + testcases := []struct { + name string + scheme *runtime.Scheme + cfg *configapi.Configuration + wantResult map[string]any + }{ + + { + name: "empty", + scheme: testScheme, + cfg: &configapi.Configuration{}, + wantResult: map[string]any{ + "apiVersion": "config.lws.x-k8s.io/v1alpha1", + "kind": "Configuration", + "health": map[string]any{}, + "metrics": map[string]any{}, + "webhook": map[string]any{}, + }, + }, + { + name: "default", + scheme: testScheme, + cfg: defaultConfig, + wantResult: map[string]any{ + "apiVersion": "config.lws.x-k8s.io/v1alpha1", + "kind": "Configuration", + "webhook": map[string]any{ + "port": int64(configapi.DefaultWebhookPort), + }, + "metrics": map[string]any{ + "bindAddress": configapi.DefaultMetricsBindAddress, + }, + "health": map[string]any{ + "healthProbeBindAddress": configapi.DefaultHealthProbeBindAddress, + }, + "leaderElection": map[string]any{ + "leaderElect": true, + "leaseDuration": configapi.DefaultLeaderElectionLeaseDuration.String(), + "renewDeadline": configapi.DefaultLeaderElectionRenewDeadline.String(), + "retryPeriod": configapi.DefaultLeaderElectionRetryPeriod.String(), + "resourceLock": resourcelock.LeasesResourceLock, + "resourceName": configapi.DefaultLeaderElectionID, + "resourceNamespace": "", + }, + "internalCertManagement": map[string]any{ + "enable": true, + "webhookServiceName": configapi.DefaultWebhookServiceName, + "webhookSecretName": configapi.DefaultWebhookSecretName, + }, + "clientConnection": map[string]any{ + "burst": int64(configapi.DefaultClientConnectionBurst), + "qps": int64(configapi.DefaultClientConnectionQPS), + }, + }, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + got, err := Encode(tc.scheme, tc.cfg) + if err != nil { + t.Errorf("Unexpected error:%s", err) + } + gotMap := map[string]interface{}{} + err = yaml.Unmarshal([]byte(got), &gotMap) + if err != nil { + t.Errorf("Unable to unmarshal result:%s", err) + } + if diff := cmp.Diff(tc.wantResult, gotMap); diff != "" { + t.Errorf("Unexpected result (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/config/validation.go b/pkg/config/validation.go new file mode 100644 index 00000000..44ebb87d --- /dev/null +++ b/pkg/config/validation.go @@ -0,0 +1,55 @@ +/* +Copyright 2025. + +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 config + +import ( + "strings" + + apimachineryvalidation "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" + + configapi "sigs.k8s.io/lws/api/config/v1alpha1" +) + +var ( + internalCertManagementPath = field.NewPath("internalCertManagement") +) + +func validate(c *configapi.Configuration) field.ErrorList { + var allErrs field.ErrorList + allErrs = append(allErrs, validateInternalCertManagement(c)...) + return allErrs +} + +func validateInternalCertManagement(c *configapi.Configuration) field.ErrorList { + var allErrs field.ErrorList + if c.InternalCertManagement == nil || !ptr.Deref(c.InternalCertManagement.Enable, false) { + return allErrs + } + if svcName := c.InternalCertManagement.WebhookServiceName; svcName != nil { + if errs := apimachineryvalidation.IsDNS1035Label(*svcName); len(errs) != 0 { + allErrs = append(allErrs, field.Invalid(internalCertManagementPath.Child("webhookServiceName"), svcName, strings.Join(errs, ","))) + } + } + if secName := c.InternalCertManagement.WebhookSecretName; secName != nil { + if errs := apimachineryvalidation.IsDNS1123Subdomain(*secName); len(errs) != 0 { + allErrs = append(allErrs, field.Invalid(internalCertManagementPath.Child("webhookSecretName"), secName, strings.Join(errs, ","))) + } + } + return allErrs +} diff --git a/pkg/config/validation_test.go b/pkg/config/validation_test.go new file mode 100644 index 00000000..cd85a2dd --- /dev/null +++ b/pkg/config/validation_test.go @@ -0,0 +1,89 @@ +/* +Copyright 2025 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 config + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" + + configapi "sigs.k8s.io/lws/api/config/v1alpha1" +) + +func TestValidate(t *testing.T) { + testCases := map[string]struct { + cfg *configapi.Configuration + wantErr field.ErrorList + }{ + "invalid .internalCertManagement.webhookSecretName": { + cfg: &configapi.Configuration{ + InternalCertManagement: &configapi.InternalCertManagement{ + Enable: ptr.To(true), + WebhookSecretName: ptr.To(":)"), + }, + }, + wantErr: field.ErrorList{ + &field.Error{ + Type: field.ErrorTypeInvalid, + Field: "internalCertManagement.webhookSecretName", + }, + }, + }, + "invalid .internalCertManagement.webhookServiceName": { + cfg: &configapi.Configuration{ + InternalCertManagement: &configapi.InternalCertManagement{ + Enable: ptr.To(true), + WebhookServiceName: ptr.To("0-invalid"), + }, + }, + wantErr: field.ErrorList{ + &field.Error{ + Type: field.ErrorTypeInvalid, + Field: "internalCertManagement.webhookServiceName", + }, + }, + }, + "disabled .internalCertManagement with invalid .internalCertManagement.webhookServiceName": { + cfg: &configapi.Configuration{ + InternalCertManagement: &configapi.InternalCertManagement{ + Enable: ptr.To(false), + WebhookServiceName: ptr.To("0-invalid"), + }, + }, + }, + "valid .internalCertManagement": { + cfg: &configapi.Configuration{ + InternalCertManagement: &configapi.InternalCertManagement{ + Enable: ptr.To(true), + WebhookServiceName: ptr.To("webhook-svc"), + WebhookSecretName: ptr.To("webhook-sec"), + }, + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + if diff := cmp.Diff(tc.wantErr, validate(tc.cfg), cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")); diff != "" { + t.Errorf("Unexpected returned error (-want,+got):\n%s", diff) + } + }) + } +} From 6239bc47bd2740794d08c7163f552d572d325f30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arda=20G=C3=BC=C3=A7l=C3=BC?= Date: Mon, 27 Jan 2025 09:19:46 +0300 Subject: [PATCH 2/6] Use default values in config file for secret and service --- pkg/cert/cert.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pkg/cert/cert.go b/pkg/cert/cert.go index 8de62e80..00ff759d 100644 --- a/pkg/cert/cert.go +++ b/pkg/cert/cert.go @@ -22,8 +22,6 @@ import ( ) const ( - serviceName = "lws-webhook-service" - secretName = "lws-webhook-server-cert" certDir = "/tmp/k8s-webhook-server/serving-certs" validateWebhookConfName = "lws-validating-webhook-configuration" mutatingWebhookConfName = "lws-mutating-webhook-configuration" @@ -37,13 +35,6 @@ const ( // CertsManager creates certs for webhooks. func CertsManager(mgr ctrl.Manager, namespace string, configServiceName string, configSecretName string, setupFinish chan struct{}) error { - if configServiceName == "" { - configServiceName = serviceName - } - if configSecretName == "" { - configSecretName = secretName - } - // dnsName is the format of ..svc var dnsName = fmt.Sprintf("%s.%s.svc", configServiceName, namespace) From 6f0c7fa8cee995480e9de3c8568e7901a71fccd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arda=20G=C3=BC=C3=A7l=C3=BC?= Date: Mon, 27 Jan 2025 11:04:27 +0300 Subject: [PATCH 3/6] Wire more config fields --- api/config/v1alpha1/defaults.go | 12 ++++++++++++ cmd/main.go | 2 +- pkg/cert/cert.go | 5 ++--- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/api/config/v1alpha1/defaults.go b/api/config/v1alpha1/defaults.go index 1646bd35..f7a9557c 100644 --- a/api/config/v1alpha1/defaults.go +++ b/api/config/v1alpha1/defaults.go @@ -24,10 +24,13 @@ import ( ) const ( + DefaultWebhookCertDir = "/tmp/k8s-webhook-server/serving-certs" DefaultWebhookServiceName = "lws-webhook-service" DefaultWebhookSecretName = "lws-webhook-server-cert" DefaultWebhookPort = 9443 DefaultHealthProbeBindAddress = ":8081" + DefaultReadinessEndpoint = "/readyz" + DefaultLivenessEndpoint = "/healthz" DefaultMetricsBindAddress = ":8443" DefaultLeaderElectionID = "b8b2488c.x-k8s.io" DefaultLeaderElectionLeaseDuration = 15 * time.Second @@ -45,12 +48,21 @@ func SetDefaults_Configuration(cfg *Configuration) { if cfg.Webhook.Port == nil { cfg.Webhook.Port = ptr.To(DefaultWebhookPort) } + if cfg.Webhook.CertDir == "" { + cfg.Webhook.CertDir = DefaultWebhookCertDir + } if len(cfg.Metrics.BindAddress) == 0 { cfg.Metrics.BindAddress = DefaultMetricsBindAddress } if len(cfg.Health.HealthProbeBindAddress) == 0 { cfg.Health.HealthProbeBindAddress = DefaultHealthProbeBindAddress } + if cfg.Health.LivenessEndpointName == "" { + cfg.Health.LivenessEndpointName = DefaultLivenessEndpoint + } + if cfg.Health.ReadinessEndpointName == "" { + cfg.Health.ReadinessEndpointName = DefaultReadinessEndpoint + } if cfg.LeaderElection == nil { cfg.LeaderElection = &configv1alpha1.LeaderElectionConfiguration{} diff --git a/cmd/main.go b/cmd/main.go index 10e7a1aa..45be7e70 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -169,7 +169,7 @@ func main() { certsReady := make(chan struct{}) if cfg.InternalCertManagement != nil && *cfg.InternalCertManagement.Enable { - if err = cert.CertsManager(mgr, namespace, *cfg.InternalCertManagement.WebhookServiceName, *cfg.InternalCertManagement.WebhookSecretName, certsReady); err != nil { + if err = cert.CertsManager(mgr, namespace, *cfg.InternalCertManagement.WebhookServiceName, *cfg.InternalCertManagement.WebhookSecretName, cfg.Webhook.CertDir, certsReady); err != nil { setupLog.Error(err, "unable to setup cert rotation") os.Exit(1) } diff --git a/pkg/cert/cert.go b/pkg/cert/cert.go index 00ff759d..a5e81b91 100644 --- a/pkg/cert/cert.go +++ b/pkg/cert/cert.go @@ -22,7 +22,6 @@ import ( ) const ( - certDir = "/tmp/k8s-webhook-server/serving-certs" validateWebhookConfName = "lws-validating-webhook-configuration" mutatingWebhookConfName = "lws-mutating-webhook-configuration" caName = "lws-ca" @@ -34,7 +33,7 @@ const ( //+kubebuilder:rbac:groups="admissionregistration.k8s.io",resources=validatingwebhookconfigurations,verbs=get;list;watch;update // CertsManager creates certs for webhooks. -func CertsManager(mgr ctrl.Manager, namespace string, configServiceName string, configSecretName string, setupFinish chan struct{}) error { +func CertsManager(mgr ctrl.Manager, namespace string, configServiceName string, configSecretName string, webhookCertDir string, setupFinish chan struct{}) error { // dnsName is the format of ..svc var dnsName = fmt.Sprintf("%s.%s.svc", configServiceName, namespace) @@ -43,7 +42,7 @@ func CertsManager(mgr ctrl.Manager, namespace string, configServiceName string, Namespace: namespace, Name: configSecretName, }, - CertDir: certDir, + CertDir: webhookCertDir, CAName: caName, CAOrganization: caOrg, DNSName: dnsName, From 2b9d797e557edf253802885a4d2a8ebb5b769ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arda=20G=C3=BC=C3=A7l=C3=BC?= Date: Mon, 27 Jan 2025 11:15:44 +0300 Subject: [PATCH 4/6] React unit tests for the new defaults of config --- api/config/v1alpha1/defaults_test.go | 20 ++++++++++++++++---- config/rbac/role.yaml | 12 +----------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/api/config/v1alpha1/defaults_test.go b/api/config/v1alpha1/defaults_test.go index 24e280fd..b2604f6d 100644 --- a/api/config/v1alpha1/defaults_test.go +++ b/api/config/v1alpha1/defaults_test.go @@ -43,13 +43,16 @@ func TestSetDefaults_Configuration(t *testing.T) { ResourceName: DefaultLeaderElectionID, }, Webhook: ControllerWebhook{ - Port: ptr.To(DefaultWebhookPort), + Port: ptr.To(DefaultWebhookPort), + CertDir: DefaultWebhookCertDir, }, Metrics: ControllerMetrics{ BindAddress: DefaultMetricsBindAddress, }, Health: ControllerHealth{ HealthProbeBindAddress: DefaultHealthProbeBindAddress, + ReadinessEndpointName: DefaultReadinessEndpoint, + LivenessEndpointName: DefaultLivenessEndpoint, }, } defaultClientConnection := &ClientConnection{ @@ -89,13 +92,16 @@ func TestSetDefaults_Configuration(t *testing.T) { want: &Configuration{ ControllerManager: ControllerManager{ Webhook: ControllerWebhook{ - Port: ptr.To(DefaultWebhookPort), + Port: ptr.To(DefaultWebhookPort), + CertDir: DefaultWebhookCertDir, }, Metrics: ControllerMetrics{ BindAddress: DefaultMetricsBindAddress, }, Health: ControllerHealth{ HealthProbeBindAddress: DefaultHealthProbeBindAddress, + ReadinessEndpointName: DefaultReadinessEndpoint, + LivenessEndpointName: DefaultLivenessEndpoint, }, LeaderElection: &configv1alpha1.LeaderElectionConfiguration{ LeaderElect: ptr.To(true), @@ -140,13 +146,16 @@ func TestSetDefaults_Configuration(t *testing.T) { want: &Configuration{ ControllerManager: ControllerManager{ Webhook: ControllerWebhook{ - Port: ptr.To(overwriteWebhookPort), + Port: ptr.To(overwriteWebhookPort), + CertDir: DefaultWebhookCertDir, }, Metrics: ControllerMetrics{ BindAddress: overwriteMetricBindAddress, }, Health: ControllerHealth{ HealthProbeBindAddress: overwriteHealthProbeBindAddress, + ReadinessEndpointName: DefaultReadinessEndpoint, + LivenessEndpointName: DefaultLivenessEndpoint, }, LeaderElection: &configv1alpha1.LeaderElectionConfiguration{ LeaderElect: ptr.To(true), @@ -177,13 +186,16 @@ func TestSetDefaults_Configuration(t *testing.T) { want: &Configuration{ ControllerManager: ControllerManager{ Webhook: ControllerWebhook{ - Port: ptr.To(DefaultWebhookPort), + Port: ptr.To(DefaultWebhookPort), + CertDir: DefaultWebhookCertDir, }, Metrics: ControllerMetrics{ BindAddress: DefaultMetricsBindAddress, }, Health: ControllerHealth{ HealthProbeBindAddress: DefaultHealthProbeBindAddress, + ReadinessEndpointName: DefaultReadinessEndpoint, + LivenessEndpointName: DefaultLivenessEndpoint, }, LeaderElection: &configv1alpha1.LeaderElectionConfiguration{ LeaderElect: ptr.To(false), diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 5e9edd30..5167252d 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -28,6 +28,7 @@ rules: - apiGroups: - "" resources: + - pods - services verbs: - create @@ -37,17 +38,6 @@ rules: - patch - update - watch -- apiGroups: - - "" - resources: - - pods - verbs: - - delete - - get - - list - - patch - - update - - watch - apiGroups: - "" resources: From 9890e3885ea275b38f90473f487b6e8be5546ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arda=20G=C3=BC=C3=A7l=C3=BC?= Date: Mon, 27 Jan 2025 11:27:42 +0300 Subject: [PATCH 5/6] Fix more unit tests due to changes in config defaulting --- pkg/config/config_test.go | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index c9c66197..87b8a497 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -54,6 +54,7 @@ apiVersion: config.lws.x-k8s.io/v1alpha1 kind: Configuration health: healthProbeBindAddress: :38081 + readinessEndpointName: test metrics: bindAddress: :38080 leaderElection: @@ -161,6 +162,8 @@ webhook: defaultControlOptions := ctrl.Options{ HealthProbeBindAddress: configapi.DefaultHealthProbeBindAddress, + ReadinessEndpointName: configapi.DefaultReadinessEndpoint, + LivenessEndpointName: configapi.DefaultLivenessEndpoint, Metrics: metricsserver.Options{ BindAddress: configapi.DefaultMetricsBindAddress, }, @@ -172,7 +175,8 @@ webhook: RetryPeriod: ptr.To(configapi.DefaultLeaderElectionRetryPeriod), WebhookServer: &webhook.DefaultServer{ Options: webhook.Options{ - Port: configapi.DefaultWebhookPort, + Port: configapi.DefaultWebhookPort, + CertDir: configapi.DefaultWebhookCertDir, }, }, } @@ -218,6 +222,8 @@ webhook: }, wantOptions: ctrl.Options{ HealthProbeBindAddress: configapi.DefaultHealthProbeBindAddress, + ReadinessEndpointName: configapi.DefaultReadinessEndpoint, + LivenessEndpointName: configapi.DefaultLivenessEndpoint, Metrics: metricsserver.Options{ BindAddress: configapi.DefaultMetricsBindAddress, }, @@ -229,7 +235,8 @@ webhook: RetryPeriod: ptr.To(configapi.DefaultLeaderElectionRetryPeriod), WebhookServer: &webhook.DefaultServer{ Options: webhook.Options{ - Port: configapi.DefaultWebhookPort, + Port: configapi.DefaultWebhookPort, + CertDir: configapi.DefaultWebhookCertDir, }, }, }, @@ -256,6 +263,8 @@ webhook: }, wantOptions: ctrl.Options{ HealthProbeBindAddress: ":38081", + ReadinessEndpointName: "test", + LivenessEndpointName: configapi.DefaultLivenessEndpoint, Metrics: metricsserver.Options{ BindAddress: ":38080", }, @@ -267,7 +276,8 @@ webhook: RetryPeriod: ptr.To(configapi.DefaultLeaderElectionRetryPeriod), WebhookServer: &webhook.DefaultServer{ Options: webhook.Options{ - Port: 9444, + Port: 9444, + CertDir: configapi.DefaultWebhookCertDir, }, }, }, @@ -317,6 +327,8 @@ webhook: }, wantOptions: ctrl.Options{ HealthProbeBindAddress: configapi.DefaultHealthProbeBindAddress, + ReadinessEndpointName: configapi.DefaultReadinessEndpoint, + LivenessEndpointName: configapi.DefaultLivenessEndpoint, Metrics: metricsserver.Options{ BindAddress: configapi.DefaultMetricsBindAddress, }, @@ -328,7 +340,8 @@ webhook: LeaderElection: false, WebhookServer: &webhook.DefaultServer{ Options: webhook.Options{ - Port: configapi.DefaultWebhookPort, + Port: configapi.DefaultWebhookPort, + CertDir: configapi.DefaultWebhookCertDir, }, }, }, @@ -417,13 +430,16 @@ func TestEncode(t *testing.T) { "apiVersion": "config.lws.x-k8s.io/v1alpha1", "kind": "Configuration", "webhook": map[string]any{ - "port": int64(configapi.DefaultWebhookPort), + "port": int64(configapi.DefaultWebhookPort), + "certDir": configapi.DefaultWebhookCertDir, }, "metrics": map[string]any{ "bindAddress": configapi.DefaultMetricsBindAddress, }, "health": map[string]any{ "healthProbeBindAddress": configapi.DefaultHealthProbeBindAddress, + "readinessEndpointName": configapi.DefaultReadinessEndpoint, + "livenessEndpointName": configapi.DefaultLivenessEndpoint, }, "leaderElection": map[string]any{ "leaderElect": true, From 6174e09b50d6c1069e7e1b5d823d6c4779c0a598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arda=20G=C3=BC=C3=A7l=C3=BC?= Date: Tue, 28 Jan 2025 07:51:41 +0300 Subject: [PATCH 6/6] Use upstream etcd defaults for durations --- api/config/v1alpha1/defaults.go | 29 +++++++++------------- api/config/v1alpha1/defaults_test.go | 35 +++++++++++++++----------- pkg/config/config_test.go | 37 +++++++++++++++++----------- 3 files changed, 54 insertions(+), 47 deletions(-) diff --git a/api/config/v1alpha1/defaults.go b/api/config/v1alpha1/defaults.go index f7a9557c..10fabc2e 100644 --- a/api/config/v1alpha1/defaults.go +++ b/api/config/v1alpha1/defaults.go @@ -17,28 +17,23 @@ limitations under the License. package v1alpha1 import ( - "time" - configv1alpha1 "k8s.io/component-base/config/v1alpha1" "k8s.io/utils/ptr" ) const ( - DefaultWebhookCertDir = "/tmp/k8s-webhook-server/serving-certs" - DefaultWebhookServiceName = "lws-webhook-service" - DefaultWebhookSecretName = "lws-webhook-server-cert" - DefaultWebhookPort = 9443 - DefaultHealthProbeBindAddress = ":8081" - DefaultReadinessEndpoint = "/readyz" - DefaultLivenessEndpoint = "/healthz" - DefaultMetricsBindAddress = ":8443" - DefaultLeaderElectionID = "b8b2488c.x-k8s.io" - DefaultLeaderElectionLeaseDuration = 15 * time.Second - DefaultLeaderElectionRenewDeadline = 10 * time.Second - DefaultLeaderElectionRetryPeriod = 2 * time.Second - DefaultResourceLock = "leases" - DefaultClientConnectionQPS float32 = 500 - DefaultClientConnectionBurst int32 = 500 + DefaultWebhookCertDir = "/tmp/k8s-webhook-server/serving-certs" + DefaultWebhookServiceName = "lws-webhook-service" + DefaultWebhookSecretName = "lws-webhook-server-cert" + DefaultWebhookPort = 9443 + DefaultHealthProbeBindAddress = ":8081" + DefaultReadinessEndpoint = "/readyz" + DefaultLivenessEndpoint = "/healthz" + DefaultMetricsBindAddress = ":8443" + DefaultLeaderElectionID = "b8b2488c.x-k8s.io" + DefaultResourceLock = "leases" + DefaultClientConnectionQPS float32 = 500 + DefaultClientConnectionBurst int32 = 500 ) // SetDefaults_Configuration sets default values for ComponentConfig. diff --git a/api/config/v1alpha1/defaults_test.go b/api/config/v1alpha1/defaults_test.go index b2604f6d..06db3df4 100644 --- a/api/config/v1alpha1/defaults_test.go +++ b/api/config/v1alpha1/defaults_test.go @@ -18,6 +18,7 @@ package v1alpha1 import ( "testing" + "time" "github.com/google/go-cmp/cmp" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,15 +31,19 @@ const ( overwriteMetricBindAddress = ":38081" overwriteHealthProbeBindAddress = ":38080" overwriteLeaderElectionID = "foo.lws.x-k8s.io" + + defaultLeaderElectionLeaseDuration = 15 * time.Second + defaultLeaderElectionRenewDeadline = 10 * time.Second + defaultLeaderElectionRetryPeriod = 2 * time.Second ) func TestSetDefaults_Configuration(t *testing.T) { defaultCtrlManagerConfigurationSpec := ControllerManager{ LeaderElection: &configv1alpha1.LeaderElectionConfiguration{ LeaderElect: ptr.To(true), - LeaseDuration: metav1.Duration{Duration: DefaultLeaderElectionLeaseDuration}, - RenewDeadline: metav1.Duration{Duration: DefaultLeaderElectionRenewDeadline}, - RetryPeriod: metav1.Duration{Duration: DefaultLeaderElectionRetryPeriod}, + LeaseDuration: metav1.Duration{Duration: defaultLeaderElectionLeaseDuration}, + RenewDeadline: metav1.Duration{Duration: defaultLeaderElectionRenewDeadline}, + RetryPeriod: metav1.Duration{Duration: defaultLeaderElectionRetryPeriod}, ResourceLock: DefaultResourceLock, ResourceName: DefaultLeaderElectionID, }, @@ -105,9 +110,9 @@ func TestSetDefaults_Configuration(t *testing.T) { }, LeaderElection: &configv1alpha1.LeaderElectionConfiguration{ LeaderElect: ptr.To(true), - LeaseDuration: metav1.Duration{Duration: DefaultLeaderElectionLeaseDuration}, - RenewDeadline: metav1.Duration{Duration: DefaultLeaderElectionRenewDeadline}, - RetryPeriod: metav1.Duration{Duration: DefaultLeaderElectionRetryPeriod}, + LeaseDuration: metav1.Duration{Duration: defaultLeaderElectionLeaseDuration}, + RenewDeadline: metav1.Duration{Duration: defaultLeaderElectionRenewDeadline}, + RetryPeriod: metav1.Duration{Duration: defaultLeaderElectionRetryPeriod}, ResourceLock: DefaultResourceLock, ResourceName: DefaultLeaderElectionID, }, @@ -132,9 +137,9 @@ func TestSetDefaults_Configuration(t *testing.T) { }, LeaderElection: &configv1alpha1.LeaderElectionConfiguration{ LeaderElect: ptr.To(true), - LeaseDuration: metav1.Duration{Duration: DefaultLeaderElectionLeaseDuration}, - RenewDeadline: metav1.Duration{Duration: DefaultLeaderElectionRenewDeadline}, - RetryPeriod: metav1.Duration{Duration: DefaultLeaderElectionRetryPeriod}, + LeaseDuration: metav1.Duration{Duration: defaultLeaderElectionLeaseDuration}, + RenewDeadline: metav1.Duration{Duration: defaultLeaderElectionRenewDeadline}, + RetryPeriod: metav1.Duration{Duration: defaultLeaderElectionRetryPeriod}, ResourceLock: DefaultResourceLock, ResourceName: overwriteLeaderElectionID, }, @@ -159,9 +164,9 @@ func TestSetDefaults_Configuration(t *testing.T) { }, LeaderElection: &configv1alpha1.LeaderElectionConfiguration{ LeaderElect: ptr.To(true), - LeaseDuration: metav1.Duration{Duration: DefaultLeaderElectionLeaseDuration}, - RenewDeadline: metav1.Duration{Duration: DefaultLeaderElectionRenewDeadline}, - RetryPeriod: metav1.Duration{Duration: DefaultLeaderElectionRetryPeriod}, + LeaseDuration: metav1.Duration{Duration: defaultLeaderElectionLeaseDuration}, + RenewDeadline: metav1.Duration{Duration: defaultLeaderElectionRenewDeadline}, + RetryPeriod: metav1.Duration{Duration: defaultLeaderElectionRetryPeriod}, ResourceLock: DefaultResourceLock, ResourceName: overwriteLeaderElectionID, }, @@ -199,9 +204,9 @@ func TestSetDefaults_Configuration(t *testing.T) { }, LeaderElection: &configv1alpha1.LeaderElectionConfiguration{ LeaderElect: ptr.To(false), - LeaseDuration: metav1.Duration{Duration: DefaultLeaderElectionLeaseDuration}, - RenewDeadline: metav1.Duration{Duration: DefaultLeaderElectionRenewDeadline}, - RetryPeriod: metav1.Duration{Duration: DefaultLeaderElectionRetryPeriod}, + LeaseDuration: metav1.Duration{Duration: defaultLeaderElectionLeaseDuration}, + RenewDeadline: metav1.Duration{Duration: defaultLeaderElectionRenewDeadline}, + RetryPeriod: metav1.Duration{Duration: defaultLeaderElectionRetryPeriod}, ResourceLock: DefaultResourceLock, ResourceName: DefaultLeaderElectionID, }, diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 87b8a497..ce1b9c17 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -23,6 +23,7 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -39,6 +40,12 @@ import ( configapi "sigs.k8s.io/lws/api/config/v1alpha1" ) +const ( + defaultLeaderElectionLeaseDuration = 15 * time.Second + defaultLeaderElectionRenewDeadline = 10 * time.Second + defaultLeaderElectionRetryPeriod = 2 * time.Second +) + func TestLoad(t *testing.T) { testScheme := runtime.NewScheme() err := configapi.AddToScheme(testScheme) @@ -170,9 +177,9 @@ webhook: LeaderElection: true, LeaderElectionID: configapi.DefaultLeaderElectionID, LeaderElectionResourceLock: resourcelock.LeasesResourceLock, - LeaseDuration: ptr.To(configapi.DefaultLeaderElectionLeaseDuration), - RenewDeadline: ptr.To(configapi.DefaultLeaderElectionRenewDeadline), - RetryPeriod: ptr.To(configapi.DefaultLeaderElectionRetryPeriod), + LeaseDuration: ptr.To(defaultLeaderElectionLeaseDuration), + RenewDeadline: ptr.To(defaultLeaderElectionRenewDeadline), + RetryPeriod: ptr.To(defaultLeaderElectionRetryPeriod), WebhookServer: &webhook.DefaultServer{ Options: webhook.Options{ Port: configapi.DefaultWebhookPort, @@ -230,9 +237,9 @@ webhook: LeaderElection: true, LeaderElectionID: configapi.DefaultLeaderElectionID, LeaderElectionResourceLock: resourcelock.LeasesResourceLock, - LeaseDuration: ptr.To(configapi.DefaultLeaderElectionLeaseDuration), - RenewDeadline: ptr.To(configapi.DefaultLeaderElectionRenewDeadline), - RetryPeriod: ptr.To(configapi.DefaultLeaderElectionRetryPeriod), + LeaseDuration: ptr.To(defaultLeaderElectionLeaseDuration), + RenewDeadline: ptr.To(defaultLeaderElectionRenewDeadline), + RetryPeriod: ptr.To(defaultLeaderElectionRetryPeriod), WebhookServer: &webhook.DefaultServer{ Options: webhook.Options{ Port: configapi.DefaultWebhookPort, @@ -271,9 +278,9 @@ webhook: LeaderElection: true, LeaderElectionID: "test-id", LeaderElectionResourceLock: resourcelock.LeasesResourceLock, - LeaseDuration: ptr.To(configapi.DefaultLeaderElectionLeaseDuration), - RenewDeadline: ptr.To(configapi.DefaultLeaderElectionRenewDeadline), - RetryPeriod: ptr.To(configapi.DefaultLeaderElectionRetryPeriod), + LeaseDuration: ptr.To(defaultLeaderElectionLeaseDuration), + RenewDeadline: ptr.To(defaultLeaderElectionRenewDeadline), + RetryPeriod: ptr.To(defaultLeaderElectionRetryPeriod), WebhookServer: &webhook.DefaultServer{ Options: webhook.Options{ Port: 9444, @@ -334,9 +341,9 @@ webhook: }, LeaderElectionID: configapi.DefaultLeaderElectionID, LeaderElectionResourceLock: resourcelock.LeasesResourceLock, - LeaseDuration: ptr.To(configapi.DefaultLeaderElectionLeaseDuration), - RenewDeadline: ptr.To(configapi.DefaultLeaderElectionRenewDeadline), - RetryPeriod: ptr.To(configapi.DefaultLeaderElectionRetryPeriod), + LeaseDuration: ptr.To(defaultLeaderElectionLeaseDuration), + RenewDeadline: ptr.To(defaultLeaderElectionRenewDeadline), + RetryPeriod: ptr.To(defaultLeaderElectionRetryPeriod), LeaderElection: false, WebhookServer: &webhook.DefaultServer{ Options: webhook.Options{ @@ -443,9 +450,9 @@ func TestEncode(t *testing.T) { }, "leaderElection": map[string]any{ "leaderElect": true, - "leaseDuration": configapi.DefaultLeaderElectionLeaseDuration.String(), - "renewDeadline": configapi.DefaultLeaderElectionRenewDeadline.String(), - "retryPeriod": configapi.DefaultLeaderElectionRetryPeriod.String(), + "leaseDuration": defaultLeaderElectionLeaseDuration.String(), + "renewDeadline": defaultLeaderElectionRenewDeadline.String(), + "retryPeriod": defaultLeaderElectionRetryPeriod.String(), "resourceLock": resourcelock.LeasesResourceLock, "resourceName": configapi.DefaultLeaderElectionID, "resourceNamespace": "",