From cc5dd05336e75678ac98bcbc5609c685e09ead05 Mon Sep 17 00:00:00 2001 From: Payal Jain Date: Wed, 21 Oct 2026 20:14:13 +0530 Subject: [PATCH] :sparkles: Add AgentConfiguration to KlusterletSpec Add an optional agentConfiguration.bindConfiguration to the Klusterlet spec so the health probe port, metrics port and host networking of the klusterlet agents can be configured. This avoids port conflicts on the node when the agents run with hostNetwork. The structure mirrors the BindConfiguration used by ClusterManager webhooks. Fixes #439 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Payal Jain --- ...cluster-management.io_klusterlets.crd.yaml | 32 ++++++++++++ operator/v1/types_klusterlet.go | 34 +++++++++++++ operator/v1/zz_generated.deepcopy.go | 42 +++++++++++++++ operator/v1/zz_generated.model_name.go | 10 ++++ test/integration/api/klusterlet_test.go | 51 +++++++++++++++++++ 5 files changed, 169 insertions(+) diff --git a/operator/v1/0000_00_operator.open-cluster-management.io_klusterlets.crd.yaml b/operator/v1/0000_00_operator.open-cluster-management.io_klusterlets.crd.yaml index 34660036..38b1d35b 100644 --- a/operator/v1/0000_00_operator.open-cluster-management.io_klusterlets.crd.yaml +++ b/operator/v1/0000_00_operator.open-cluster-management.io_klusterlets.crd.yaml @@ -43,6 +43,38 @@ spec: description: Spec represents the desired deployment configuration of Klusterlet agent. properties: + agentConfiguration: + description: |- + AgentConfiguration contains the configuration of the klusterlet agents, + such as the bind ports of their health probe and metrics endpoints. + properties: + bindConfiguration: + description: BindConfiguration represents the server bind configuration + of the klusterlet agents. + properties: + healthProbePort: + default: 8443 + description: |- + HealthProbePort represents the bind port of the agents' health check endpoint. The default value is 8443. + Agents run as non-root, so privileged ports below 1024 are not allowed. + format: int32 + maximum: 65535 + minimum: 1024 + type: integer + hostNetwork: + description: HostNetwork enables running the agent pods in + host networking mode. + type: boolean + metricsPort: + default: 8080 + description: |- + MetricsPort represents the bind port of the agents' metrics endpoint. The default value is 8080. + Metrics may be disabled by setting a value less than or equal to 0. + format: int32 + maximum: 65535 + type: integer + type: object + type: object clusterName: description: |- clusterName is the name of the managed cluster to be created on hub. diff --git a/operator/v1/types_klusterlet.go b/operator/v1/types_klusterlet.go index 6906131e..04449577 100644 --- a/operator/v1/types_klusterlet.go +++ b/operator/v1/types_klusterlet.go @@ -100,6 +100,40 @@ type KlusterletSpec struct { // is not available on the managed cluster. // +optional PriorityClassName string `json:"priorityClassName,omitempty"` + + // AgentConfiguration contains the configuration of the klusterlet agents, + // such as the bind ports of their health probe and metrics endpoints. + // +optional + AgentConfiguration *AgentConfiguration `json:"agentConfiguration,omitempty"` +} + +// AgentConfiguration represents customization of the klusterlet agents. +type AgentConfiguration struct { + // BindConfiguration represents the server bind configuration of the klusterlet agents. + // +optional + BindConfiguration *AgentBindConfiguration `json:"bindConfiguration,omitempty"` +} + +// AgentBindConfiguration represents customization of the server bindings of the klusterlet agents. +type AgentBindConfiguration struct { + // HealthProbePort represents the bind port of the agents' health check endpoint. The default value is 8443. + // Agents run as non-root, so privileged ports below 1024 are not allowed. + // +optional + // +kubebuilder:default=8443 + // +kubebuilder:validation:Minimum=1024 + // +kubebuilder:validation:Maximum=65535 + HealthProbePort int32 `json:"healthProbePort,omitempty"` + + // MetricsPort represents the bind port of the agents' metrics endpoint. The default value is 8080. + // Metrics may be disabled by setting a value less than or equal to 0. + // +optional + // +kubebuilder:default=8080 + // +kubebuilder:validation:Maximum=65535 + MetricsPort int32 `json:"metricsPort"` + + // HostNetwork enables running the agent pods in host networking mode. + // +optional + HostNetwork bool `json:"hostNetwork,omitempty"` } // ServerURL represents the apiserver url and ca bundle that is accessible externally diff --git a/operator/v1/zz_generated.deepcopy.go b/operator/v1/zz_generated.deepcopy.go index 9192994c..14431e86 100644 --- a/operator/v1/zz_generated.deepcopy.go +++ b/operator/v1/zz_generated.deepcopy.go @@ -54,6 +54,43 @@ func (in *AddOnRegistrationDriver) DeepCopy() *AddOnRegistrationDriver { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentBindConfiguration) DeepCopyInto(out *AgentBindConfiguration) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentBindConfiguration. +func (in *AgentBindConfiguration) DeepCopy() *AgentBindConfiguration { + if in == nil { + return nil + } + out := new(AgentBindConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AgentConfiguration) DeepCopyInto(out *AgentConfiguration) { + *out = *in + if in.BindConfiguration != nil { + in, out := &in.BindConfiguration, &out.BindConfiguration + *out = new(AgentBindConfiguration) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AgentConfiguration. +func (in *AgentConfiguration) DeepCopy() *AgentConfiguration { + if in == nil { + return nil + } + out := new(AgentConfiguration) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AwsIrsa) DeepCopyInto(out *AwsIrsa) { *out = *in @@ -720,6 +757,11 @@ func (in *KlusterletSpec) DeepCopyInto(out *KlusterletSpec) { *out = new(ResourceRequirement) (*in).DeepCopyInto(*out) } + if in.AgentConfiguration != nil { + in, out := &in.AgentConfiguration, &out.AgentConfiguration + *out = new(AgentConfiguration) + (*in).DeepCopyInto(*out) + } return } diff --git a/operator/v1/zz_generated.model_name.go b/operator/v1/zz_generated.model_name.go index 73e80ea4..1ececfaf 100644 --- a/operator/v1/zz_generated.model_name.go +++ b/operator/v1/zz_generated.model_name.go @@ -16,6 +16,16 @@ func (in AddOnRegistrationDriver) OpenAPIModelName() string { return "io.open-cluster-management.api.operator.v1.AddOnRegistrationDriver" } +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in AgentBindConfiguration) OpenAPIModelName() string { + return "io.open-cluster-management.api.operator.v1.AgentBindConfiguration" +} + +// OpenAPIModelName returns the OpenAPI model name for this type. +func (in AgentConfiguration) OpenAPIModelName() string { + return "io.open-cluster-management.api.operator.v1.AgentConfiguration" +} + // OpenAPIModelName returns the OpenAPI model name for this type. func (in AwsIrsa) OpenAPIModelName() string { return "io.open-cluster-management.api.operator.v1.AwsIrsa" diff --git a/test/integration/api/klusterlet_test.go b/test/integration/api/klusterlet_test.go index c5cb37a8..f78e9a5b 100644 --- a/test/integration/api/klusterlet_test.go +++ b/test/integration/api/klusterlet_test.go @@ -287,6 +287,57 @@ var _ = Describe("Klusterlet API test with WorkConfiguration", func() { }) }) +var _ = Describe("Klusterlet API test with AgentConfiguration", func() { + var klusterlet *operatorv1.Klusterlet + + BeforeEach(func() { + klusterlet = &operatorv1.Klusterlet{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("klusterlet-agentconfig-%s", rand.String(5)), + }, + } + }) + + AfterEach(func() { + err := operatorClient.OperatorV1().Klusterlets().Delete(context.TODO(), klusterlet.Name, metav1.DeleteOptions{}) + if !apierrors.IsNotFound(err) { + Expect(err).ToNot(HaveOccurred()) + } + }) + + withBindConfiguration := func(bind *operatorv1.AgentBindConfiguration) { + klusterlet.Spec.AgentConfiguration = &operatorv1.AgentConfiguration{BindConfiguration: bind} + } + + It("should default the health probe port and keep a zero metrics port", func() { + withBindConfiguration(&operatorv1.AgentBindConfiguration{}) + created, err := operatorClient.OperatorV1().Klusterlets().Create(context.TODO(), klusterlet, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(created.Spec.AgentConfiguration.BindConfiguration.HealthProbePort).Should(Equal(int32(8443))) + Expect(created.Spec.AgentConfiguration.BindConfiguration.MetricsPort).Should(Equal(int32(0))) + Expect(created.Spec.AgentConfiguration.BindConfiguration.HostNetwork).Should(BeFalse()) + }) + + It("should accept custom ports with host network", func() { + withBindConfiguration(&operatorv1.AgentBindConfiguration{ + HealthProbePort: 9443, + MetricsPort: 9090, + HostNetwork: true, + }) + created, err := operatorClient.OperatorV1().Klusterlets().Create(context.TODO(), klusterlet, metav1.CreateOptions{}) + Expect(err).ToNot(HaveOccurred()) + Expect(created.Spec.AgentConfiguration.BindConfiguration.HealthProbePort).Should(Equal(int32(9443))) + Expect(created.Spec.AgentConfiguration.BindConfiguration.MetricsPort).Should(Equal(int32(9090))) + Expect(created.Spec.AgentConfiguration.BindConfiguration.HostNetwork).Should(BeTrue()) + }) + + It("should reject a privileged health probe port", func() { + withBindConfiguration(&operatorv1.AgentBindConfiguration{HealthProbePort: 80}) + _, err := operatorClient.OperatorV1().Klusterlets().Create(context.TODO(), klusterlet, metav1.CreateOptions{}) + Expect(err).To(HaveOccurred()) + }) +}) + var _ = Describe("Klusterlet v1 Enhanced API test", func() { var klusterletName string