Skip to content

Commit 01869f4

Browse files
linoyaslantsorya
authored andcommitted
feat: Add MetalLB configuration for LoadBalancer service exposure
1 parent 10bac92 commit 01869f4

18 files changed

Lines changed: 1498 additions & 47 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Build the manager binary
2-
FROM golang:1.24 AS builder
2+
FROM golang:1.25 AS builder
33
ARG TARGETOS
44
ARG TARGETARCH
55

api/v1alpha1/dpfhcpprovisioner_types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ type DPFHCPProvisionerSpec struct {
105105
// Must be a routable IP in the management cluster network
106106
// This field is immutable.
107107
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="virtualIP is immutable"
108+
// +kubebuilder:validation:XValidation:rule="self == '' || isIP(self)",message="virtualIP must be a valid IP address"
108109
// +immutable
109110
// +optional
110111
VirtualIP string `json:"virtualIP,omitempty"`
@@ -194,6 +195,10 @@ const (
194195

195196
// DPUClusterInUse indicates whether the DPUCluster is already in use by another DPFHCPProvisioner.
196197
DPUClusterInUse string = "DPUClusterInUse"
198+
199+
// MetalLBConfigured indicates whether MetalLB resources (IPAddressPool and L2Advertisement)
200+
// have been successfully created and are in sync with the DPFHCPProvisioner spec.
201+
MetalLBConfigured string = "MetalLBConfigured"
197202
)
198203

199204
// Condition reasons for DPFHCPProvisioner Ready status.

cmd/main.go

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,16 @@ import (
4040
dpuprovisioningv1alpha1 "github.com/nvidia/doca-platform/api/provisioning/v1alpha1"
4141
hyperv1 "github.com/openshift/hypershift/api/hypershift/v1beta1"
4242
provisioningv1alpha1 "github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator/api/v1alpha1"
43+
"github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator/internal/common"
4344
"github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator/internal/controller"
4445
"github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator/internal/controller/bluefield"
4546
"github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator/internal/controller/dpucluster"
4647
"github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator/internal/controller/finalizer"
4748
"github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator/internal/controller/hostedcluster"
4849
"github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator/internal/controller/kubeconfiginjection"
50+
"github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator/internal/controller/metallb"
4951
"github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator/internal/controller/secrets"
52+
metallbv1beta1 "go.universe.tf/metallb/api/v1beta1"
5053
// +kubebuilder:scaffold:imports
5154
)
5255

@@ -61,6 +64,7 @@ func init() {
6164
utilruntime.Must(provisioningv1alpha1.AddToScheme(scheme))
6265
utilruntime.Must(dpuprovisioningv1alpha1.AddToScheme(scheme))
6366
utilruntime.Must(hyperv1.AddToScheme(scheme))
67+
utilruntime.Must(metallbv1beta1.AddToScheme(scheme))
6468
// +kubebuilder:scaffold:scheme
6569
}
6670

@@ -212,48 +216,60 @@ func main() {
212216
os.Exit(1)
213217
}
214218

219+
client := mgr.GetClient()
220+
recorder := mgr.GetEventRecorderFor(common.ControllerName)
221+
scheme := mgr.GetScheme()
222+
215223
// Initialize BlueField Image Resolver
216-
imageResolver := bluefield.NewImageResolver(mgr.GetClient(), mgr.GetEventRecorderFor("dpfhcpprovisioner-controller"))
224+
imageResolver := bluefield.NewImageResolver(client, recorder)
217225

218226
// Initialize DPUCluster Validator
219-
dpuClusterValidator := dpucluster.NewValidator(mgr.GetClient(), mgr.GetEventRecorderFor("dpfhcpprovisioner-controller"))
227+
dpuClusterValidator := dpucluster.NewValidator(client, recorder)
220228

221229
// Initialize Secrets Validator
222-
secretsValidator := secrets.NewValidator(mgr.GetClient(), mgr.GetEventRecorderFor("dpfhcpprovisioner-controller"))
230+
secretsValidator := secrets.NewValidator(client, recorder)
223231

224232
// Initialize Secret Manager for HostedCluster lifecycle
225-
secretManager := hostedcluster.NewSecretManager(mgr.GetClient(), mgr.GetScheme())
233+
secretManager := hostedcluster.NewSecretManager(client, scheme)
226234

227235
// Initialize HostedCluster Manager
228-
hostedClusterManager := hostedcluster.NewHostedClusterManager(mgr.GetClient(), mgr.GetScheme())
236+
hostedClusterManager := hostedcluster.NewHostedClusterManager(client, scheme)
229237

230238
// Initialize NodePool Manager
231-
nodePoolManager := hostedcluster.NewNodePoolManager(mgr.GetClient(), mgr.GetScheme())
239+
nodePoolManager := hostedcluster.NewNodePoolManager(client, scheme)
232240

233241
// Initialize Kubeconfig Injector
234-
kubeconfigInjector := kubeconfiginjection.NewKubeconfigInjector(mgr.GetClient(), mgr.GetEventRecorderFor("dpfhcpprovisioner-controller"))
242+
kubeconfigInjector := kubeconfiginjection.NewKubeconfigInjector(client, recorder)
243+
244+
// Initialize MetalLB Manager
245+
metalLBManager := metallb.NewMetalLBManager(client, recorder)
235246

236247
// Initialize Finalizer Manager with pluggable cleanup handlers
237248
// Handlers are executed in registration order
238-
finalizerManager := finalizer.NewManager(mgr.GetClient(), mgr.GetEventRecorderFor("dpfhcpprovisioner-controller"))
249+
finalizerManager := finalizer.NewManager(client, recorder)
239250

240-
// Register cleanup handlers in order (dependent resources first)
251+
// Register cleanup handlers in order (dependent resources first, dependencies last)
241252
// 1. Kubeconfig injection cleanup (removes kubeconfig from DPUCluster namespace)
242-
finalizerManager.RegisterHandler(kubeconfiginjection.NewCleanupHandler(mgr.GetClient(), mgr.GetEventRecorderFor("dpfhcpprovisioner-controller")))
243-
// 2. HostedCluster cleanup (removes HostedCluster, NodePool, and secrets)
244-
finalizerManager.RegisterHandler(hostedcluster.NewCleanupHandler(mgr.GetClient(), mgr.GetEventRecorderFor("dpfhcpprovisioner-controller")))
253+
finalizerManager.RegisterHandler(kubeconfiginjection.NewCleanupHandler(client, recorder))
254+
// 2. HostedCluster cleanup (removes HostedCluster, NodePool, services, and secrets)
255+
// Must run before MetalLB cleanup because LoadBalancer services depend on IPAddressPool
256+
finalizerManager.RegisterHandler(hostedcluster.NewCleanupHandler(client, recorder))
257+
// 3. MetalLB cleanup (removes IPAddressPool and L2Advertisement)
258+
// Must run after HostedCluster cleanup to avoid deleting IPs while services still exist
259+
finalizerManager.RegisterHandler(metallb.NewCleanupHandler(client, recorder))
245260

246261
// Initialize Status Syncer for HostedCluster status mirroring
247-
statusSyncer := hostedcluster.NewStatusSyncer(mgr.GetClient())
262+
statusSyncer := hostedcluster.NewStatusSyncer(client)
248263

249264
if err := (&controller.DPFHCPProvisionerReconciler{
250-
Client: mgr.GetClient(),
251-
Scheme: mgr.GetScheme(),
252-
Recorder: mgr.GetEventRecorderFor("dpfhcpprovisioner-controller"),
265+
Client: client,
266+
Scheme: scheme,
267+
Recorder: recorder,
253268
ImageResolver: imageResolver,
254269
DPUClusterValidator: dpuClusterValidator,
255270
SecretsValidator: secretsValidator,
256271
SecretManager: secretManager,
272+
MetalLBManager: metalLBManager,
257273
HostedClusterManager: hostedClusterManager,
258274
NodePoolManager: nodePoolManager,
259275
FinalizerManager: finalizerManager,

config/crd/bases/provisioning.dpu.hcp.io_dpfhcpprovisioners.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,8 @@ spec:
178178
x-kubernetes-validations:
179179
- message: virtualIP is immutable
180180
rule: self == oldSelf
181+
- message: virtualIP must be a valid IP address
182+
rule: self == '' || isIP(self)
181183
required:
182184
- baseDomain
183185
- dpuClusterRef

config/rbac/role.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,19 @@ rules:
7373
- nodepools/status
7474
verbs:
7575
- get
76+
- apiGroups:
77+
- metallb.io
78+
resources:
79+
- ipaddresspools
80+
- l2advertisements
81+
verbs:
82+
- create
83+
- delete
84+
- get
85+
- list
86+
- patch
87+
- update
88+
- watch
7689
- apiGroups:
7790
- provisioning.dpu.hcp.io
7891
resources:

go.mod

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
module github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator
22

3-
go 1.24.4
4-
5-
toolchain go1.24.11
3+
go 1.25
64

75
require (
86
github.com/nvidia/doca-platform v0.0.0-20251115082520-81369e955c6c
97
github.com/onsi/ginkgo/v2 v2.27.2
108
github.com/onsi/gomega v1.38.2
119
github.com/openshift/hypershift v0.1.71
1210
github.com/openshift/hypershift/api v0.0.0-20251229083354-c1d28e31a05d
11+
go.universe.tf/metallb v0.15.3
1312
k8s.io/api v0.34.2
1413
k8s.io/apimachinery v0.34.2
1514
k8s.io/client-go v0.34.2
1615
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397
17-
sigs.k8s.io/controller-runtime v0.21.0
16+
sigs.k8s.io/controller-runtime v0.22.3
1817
)
1918

2019
require (

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
189189
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
190190
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
191191
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
192+
go.universe.tf/metallb v0.15.3 h1:jmTFharsaP9yBW0p/giOahfuVNTBIvwQqxx2eYxM6fc=
193+
go.universe.tf/metallb v0.15.3/go.mod h1:bvvGwZSz20f/PH1wn1D8GRHsNXXA6ZaYtxTAzyZsQAA=
192194
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
193195
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
194196
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
@@ -278,8 +280,8 @@ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8
278280
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
279281
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.32.0 h1:XotDXzqvJ8Nx5eiZZueLpTuafJz8SiodgOemI+w87QU=
280282
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.32.0/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
281-
sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8=
282-
sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM=
283+
sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y=
284+
sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
283285
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
284286
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
285287
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=

helm/dpf-hcp-provisioner-operator/templates/clusterrole.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,18 @@ rules:
134134
- nodepools/status
135135
verbs:
136136
- get
137+
138+
# MetalLB permissions (for LoadBalancer service exposure)
139+
- apiGroups:
140+
- metallb.io
141+
resources:
142+
- ipaddresspools
143+
- l2advertisements
144+
verbs:
145+
- create
146+
- delete
147+
- get
148+
- list
149+
- patch
150+
- update
151+
- watch

internal/common/constants.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,25 @@ const (
2121
// DPFHCPProvisionerName is the resource kind name used in logs, metrics, events, etc.
2222
// If the CR is renamed, update this constant once and it propagates everywhere.
2323
DPFHCPProvisionerName = "dpfhcpprovisioner"
24+
25+
// ControllerName is the name used for event recorders
26+
ControllerName = "dpfhcpprovisioner-controller"
27+
)
28+
29+
// Label keys for cross-namespace resource ownership tracking
30+
const (
31+
// LabelDPFHCPProvisionerName is the label key for the DPFHCPProvisioner name
32+
// Used to track resources owned by a specific DPFHCPProvisioner across namespaces
33+
LabelDPFHCPProvisionerName = "dpfhcpprovisioner.dpu.hcp.io/name"
34+
35+
// LabelDPFHCPProvisionerNamespace is the label key for the DPFHCPProvisioner namespace
36+
// Used to track resources owned by a specific DPFHCPProvisioner across namespaces
37+
LabelDPFHCPProvisionerNamespace = "dpfhcpprovisioner.dpu.hcp.io/namespace"
38+
)
39+
40+
// Namespace constants
41+
const (
42+
// OpenshiftOperatorsNamespace is the namespace for OpenShift operator resources
43+
// Used for MetalLB resources and other operator-managed resources
44+
OpenshiftOperatorsNamespace = "openshift-operators"
2445
)

internal/controller/dpfhcpprovisioner_controller.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import (
4545
"github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator/internal/controller/finalizer"
4646
"github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator/internal/controller/hostedcluster"
4747
"github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator/internal/controller/kubeconfiginjection"
48+
"github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator/internal/controller/metallb"
4849
"github.com/rh-ecosystem-edge/dpf-hcp-provisioner-operator/internal/controller/secrets"
4950
)
5051

@@ -57,6 +58,7 @@ type DPFHCPProvisionerReconciler struct {
5758
DPUClusterValidator *dpucluster.Validator
5859
SecretsValidator *secrets.Validator
5960
SecretManager *hostedcluster.SecretManager
61+
MetalLBManager *metallb.MetalLBManager
6062
HostedClusterManager *hostedcluster.HostedClusterManager
6163
NodePoolManager *hostedcluster.NodePoolManager
6264
FinalizerManager *finalizer.Manager
@@ -83,6 +85,8 @@ const (
8385
// +kubebuilder:rbac:groups=hypershift.openshift.io,resources=hostedclusters/status,verbs=get
8486
// +kubebuilder:rbac:groups=hypershift.openshift.io,resources=nodepools,verbs=get;list;watch;create;update;patch;delete
8587
// +kubebuilder:rbac:groups=hypershift.openshift.io,resources=nodepools/status,verbs=get
88+
// +kubebuilder:rbac:groups=metallb.io,resources=ipaddresspools,verbs=get;list;watch;create;update;patch;delete
89+
// +kubebuilder:rbac:groups=metallb.io,resources=l2advertisements,verbs=get;list;watch;create;update;patch;delete
8690

8791
// Reconcile is part of the main kubernetes reconciliation loop which aims to
8892
// move the current state of the cluster closer to the desired state.
@@ -176,6 +180,16 @@ func (r *DPFHCPProvisionerReconciler) Reconcile(ctx context.Context, req ctrl.Re
176180
// Recompute phase after validations to ensure HostedCluster creation only proceeds if all validations pass
177181
r.updatePhaseFromConditions(&cr)
178182

183+
// Feature: MetalLB Configuration
184+
// Configure MetalLB resources (IPAddressPool and L2Advertisement) when LoadBalancer exposure is needed
185+
log.V(1).Info("Configuring MetalLB resources")
186+
if result, err := r.MetalLBManager.ConfigureMetalLB(ctx, &cr); err != nil || result.Requeue || result.RequeueAfter > 0 {
187+
if err != nil {
188+
log.Error(err, "MetalLB configuration failed")
189+
}
190+
return result, err
191+
}
192+
179193
// Feature: Copy Secrets to clusters namespace
180194
// Only run during Pending phase (all validations must pass first)
181195
// Note: We only check for Pending (not Failed) to prevent secret operations when validations fail
@@ -603,7 +617,24 @@ func conditionsEqual(oldConds, newConds []metav1.Condition) bool {
603617
func (r *DPFHCPProvisionerReconciler) computeReadyCondition(ctx context.Context, cr *provisioningv1alpha1.DPFHCPProvisioner) {
604618
log := logf.FromContext(ctx)
605619

606-
// Requirement 1: HostedCluster must be available
620+
// Requirement 1: MetalLB must be configured (if required)
621+
// This is only required when exposing services through LoadBalancer
622+
// Checked first because MetalLB configuration happens before HostedCluster creation
623+
if cr.ShouldExposeThroughLoadBalancer() {
624+
metalLBConfigured := meta.FindStatusCondition(cr.Status.Conditions, provisioningv1alpha1.MetalLBConfigured)
625+
if metalLBConfigured == nil || metalLBConfigured.Status != metav1.ConditionTrue {
626+
meta.SetStatusCondition(&cr.Status.Conditions, metav1.Condition{
627+
Type: provisioningv1alpha1.Ready,
628+
Status: metav1.ConditionFalse,
629+
Reason: "MetalLBNotConfigured",
630+
Message: "Waiting for MetalLB configuration to complete",
631+
})
632+
log.V(1).Info("Not ready: MetalLB not configured")
633+
return
634+
}
635+
}
636+
637+
// Requirement 2: HostedCluster must be available
607638
// This is set by the StatusSyncer after mirroring HostedCluster status
608639
hcAvailable := meta.FindStatusCondition(cr.Status.Conditions, provisioningv1alpha1.HostedClusterAvailable)
609640
if hcAvailable == nil || hcAvailable.Status != metav1.ConditionTrue {
@@ -617,7 +648,7 @@ func (r *DPFHCPProvisionerReconciler) computeReadyCondition(ctx context.Context,
617648
return
618649
}
619650

620-
// Requirement 2: Kubeconfig must be injected
651+
// Requirement 3: Kubeconfig must be injected
621652
// This is set by the KubeconfigInjector after successful injection
622653
kubeconfigInjected := meta.FindStatusCondition(cr.Status.Conditions, provisioningv1alpha1.KubeConfigInjected)
623654
if kubeconfigInjected == nil || kubeconfigInjected.Status != metav1.ConditionTrue {

0 commit comments

Comments
 (0)