Skip to content

Commit 453bd00

Browse files
authored
Reverse firewall precedence to prioritize direct IDs over references (#717)
Direct firewall IDs now take precedence over references for both LinodeMachine and LinodeCluster resources, bringing firewall configuration logic in line with how we handle VPC resources. This change provides more intuitive behavior and prevents reference-resolved IDs from being stored back in the spec. Documentation updated with migration notes for existing clusters.
1 parent 8426a24 commit 453bd00

11 files changed

+266
-38
lines changed

cloud/services/loadbalancers.go

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -141,27 +141,24 @@ func EnsureNodeBalancer(ctx context.Context, clusterScope *scope.ClusterScope, l
141141
}
142142
}
143143

144-
// get firewall ID from firewallRef if it exists
145-
if clusterScope.LinodeCluster.Spec.NodeBalancerFirewallRef != nil {
146-
firewallID, err := getFirewallID(ctx, clusterScope, logger)
147-
if err != nil {
148-
logger.Error(err, "Failed to fetch LinodeFirewall ID")
149-
return nil, err
150-
}
151-
createConfig.FirewallID = firewallID
152-
clusterScope.LinodeCluster.Spec.Network.NodeBalancerFirewallID = &firewallID
153-
}
154-
155-
// Use a firewall created outside of the CAPL ecosystem
156-
// get & validate firewall ID from .Spec.Network.FirewallID if it exists
144+
// First check if a direct NodeBalancerFirewallID is specified (prioritize direct ID)
157145
if clusterScope.LinodeCluster.Spec.Network.NodeBalancerFirewallID != nil {
158146
firewallID := *clusterScope.LinodeCluster.Spec.Network.NodeBalancerFirewallID
147+
logger.Info("Using direct NodeBalancerFirewallID", "firewallID", firewallID)
159148
firewall, err := clusterScope.LinodeClient.GetFirewall(ctx, firewallID)
160149
if err != nil {
161150
logger.Error(err, "Failed to fetch Linode Firewall from the Linode API")
162151
return nil, err
163152
}
164153
createConfig.FirewallID = firewall.ID
154+
} else if clusterScope.LinodeCluster.Spec.NodeBalancerFirewallRef != nil {
155+
// Only use NodeBalancerFirewallRef if no direct ID is provided
156+
firewallID, err := getFirewallID(ctx, clusterScope, logger)
157+
if err != nil {
158+
logger.Error(err, "Failed to fetch LinodeFirewall ID from reference")
159+
return nil, err
160+
}
161+
createConfig.FirewallID = firewallID
165162
}
166163

167164
return clusterScope.LinodeClient.CreateNodeBalancer(ctx, createConfig)

cloud/services/loadbalancers_test.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,6 @@ func TestEnsureNodeBalancer(t *testing.T) {
162162
return nil
163163
})
164164

165-
// Mock GetFirewall call
166-
mockClient.EXPECT().GetFirewall(gomock.Any(), 5678).Return(&linodego.Firewall{
167-
ID: 5678,
168-
}, nil)
169-
170165
// Mock CreateNodeBalancer call
171166
mockClient.EXPECT().CreateNodeBalancer(
172167
gomock.Any(),

docs/src/topics/firewalling.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -195,16 +195,27 @@ When configuring firewalls, you can specify either a direct `firewallID` or a `f
195195

196196
For `LinodeMachine` resources, when both `firewallID` and `firewallRef` are specified:
197197

198-
- `firewallRef` takes precedence over `firewallID`
199-
- The ID from the referenced `LinodeFirewall` will be used instead of the directly specified `firewallID`
198+
- `firewallID` takes precedence over `firewallRef`
199+
- The directly specified `firewallID` will be used instead of the referenced `LinodeFirewall`
200200

201201
#### LinodeCluster NodeBalancer Firewall Precedence
202202

203203
For `LinodeCluster` resources, when both `NodeBalancerFirewallID` and `NodeBalancerFirewallRef` are specified:
204204

205-
- `NodeBalancerFirewallRef` takes precedence over `NodeBalancerFirewallID`
206-
- The ID from the referenced `LinodeFirewall` will be used instead of the directly specified `NodeBalancerFirewallID`
205+
- `NodeBalancerFirewallID` takes precedence over `NodeBalancerFirewallRef`
206+
- The directly specified `NodeBalancerFirewallID` will be used instead of the referenced `LinodeFirewall`
207207

208208
```admonish warning
209-
While you can specify both direct IDs and references, it's recommended to use only one approach for clarity and to avoid confusion.
209+
While describing the precedence rules above, please note that specifying both direct IDs and references in the same resource is not recommended and will be rejected by the webhook validator. You should use either a direct ID or a reference, but not both.
210+
```
211+
212+
```admonish note title="Migration Note for Existing Clusters"
213+
In previous versions, the behavior was reversed - references took precedence over direct IDs, and the resolved ID from a reference was stored back in the direct ID field.
214+
215+
If you have existing clusters that were created with references, you may need to:
216+
1. Clear the direct ID field (`firewallID` or `NodeBalancerFirewallID`)
217+
2. Keep only the reference field (`firewallRef` or `NodeBalancerFirewallRef`)
218+
3. Allow the cluster to reconcile with the new behavior
219+
220+
This ensures that changes to your references will be properly respected.
210221
```

internal/controller/linodecluster_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ func (r *LinodeClusterReconciler) reconcilePreflightLinodeFirewallCheck(ctx cont
248248
// If NodeBalancerFirewallID is directly specified, check if it exists
249249
if clusterScope.LinodeCluster.Spec.Network.NodeBalancerFirewallID != nil {
250250
firewallID := *clusterScope.LinodeCluster.Spec.Network.NodeBalancerFirewallID
251+
logger.Info("Verifying direct NodeBalancerFirewallID", "firewallID", firewallID)
251252
_, err := clusterScope.LinodeClient.GetFirewall(ctx, firewallID)
252253
if err != nil {
253254
logger.Error(err, "Failed to get NodeBalancer firewall with provided ID", "firewallID", firewallID)

internal/controller/linodecluster_controller_test.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -190,10 +190,6 @@ var _ = Describe("cluster-lifecycle", Ordered, Label("cluster", "cluster-lifecyc
190190
if cScope.LinodeCluster.Spec.NodeBalancerFirewallRef != nil {
191191
linodeFirewall.Spec.FirewallID = util.Pointer(123)
192192
k8sClient.Update(ctx, &linodeFirewall)
193-
194-
// Mock GetFirewall call
195-
mck.LinodeClient.EXPECT().GetFirewall(gomock.Any(), gomock.Any()).
196-
Return(&linodego.Firewall{ID: 123}, nil)
197193
}
198194

199195
// If using direct firewall ID
@@ -224,10 +220,6 @@ var _ = Describe("cluster-lifecycle", Ordered, Label("cluster", "cluster-lifecyc
224220
Call("cluster is not created because nb was nil", func(ctx context.Context, mck Mock) {
225221
cScope.LinodeClient = mck.LinodeClient
226222

227-
// Mock GetFirewall call
228-
mck.LinodeClient.EXPECT().GetFirewall(gomock.Any(), gomock.Any()).
229-
Return(&linodego.Firewall{ID: 123}, nil)
230-
231223
// Mock CreateNodeBalancer to return nil
232224
mck.LinodeClient.EXPECT().CreateNodeBalancer(gomock.Any(), gomock.Any()).
233225
Return(nil, nil)
@@ -246,10 +238,6 @@ var _ = Describe("cluster-lifecycle", Ordered, Label("cluster", "cluster-lifecyc
246238
Call("cluster is not created because nb config was nil", func(ctx context.Context, mck Mock) {
247239
cScope.LinodeClient = mck.LinodeClient
248240

249-
// Mock GetFirewall call
250-
mck.LinodeClient.EXPECT().GetFirewall(gomock.Any(), gomock.Any()).
251-
Return(&linodego.Firewall{ID: 123}, nil)
252-
253241
// Mock CreateNodeBalancerConfig to return nil
254242
mck.LinodeClient.EXPECT().CreateNodeBalancerConfig(gomock.Any(), gomock.Any(), gomock.Any()).
255243
Return(nil, errors.New("nodeBalancer config created was nil"))

internal/controller/linodemachine_controller.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,28 @@ func (r *LinodeMachineReconciler) reconcilePreflightVPC(ctx context.Context, log
442442
}
443443

444444
func (r *LinodeMachineReconciler) reconcilePreflightLinodeFirewallCheck(ctx context.Context, logger logr.Logger, machineScope *scope.MachineScope) (ctrl.Result, error) {
445+
// First check if a direct FirewallID is specified
446+
if machineScope.LinodeMachine.Spec.FirewallID != 0 {
447+
logger.Info("Verifying direct FirewallID", "firewallID", machineScope.LinodeMachine.Spec.FirewallID)
448+
_, err := machineScope.LinodeClient.GetFirewall(ctx, machineScope.LinodeMachine.Spec.FirewallID)
449+
if err != nil {
450+
logger.Error(err, "Failed to get firewall with provided ID", "firewallID", machineScope.LinodeMachine.Spec.FirewallID)
451+
conditions.Set(machineScope.LinodeMachine, metav1.Condition{
452+
Type: ConditionPreflightLinodeFirewallReady,
453+
Status: metav1.ConditionFalse,
454+
Reason: util.CreateError,
455+
Message: err.Error(),
456+
})
457+
return ctrl.Result{RequeueAfter: reconciler.DefaultClusterControllerReconcileDelay}, nil
458+
}
459+
conditions.Set(machineScope.LinodeMachine, metav1.Condition{
460+
Type: ConditionPreflightLinodeFirewallReady,
461+
Status: metav1.ConditionTrue,
462+
Reason: "LinodeFirewallReady",
463+
})
464+
return ctrl.Result{}, nil
465+
}
466+
445467
// If NodeBalancerFirewallID is directly specified, check if it exists
446468
if machineScope.LinodeCluster.Spec.Network.NodeBalancerFirewallID != nil {
447469
firewallID := *machineScope.LinodeCluster.Spec.Network.NodeBalancerFirewallID

internal/controller/linodemachine_controller_helpers.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ func newCreateConfig(ctx context.Context, machineScope *scope.MachineScope, gzip
141141
}
142142

143143
// Configure firewall if needed
144-
if machineScope.LinodeMachine.Spec.FirewallRef != nil {
144+
if machineScope.LinodeMachine.Spec.FirewallRef != nil || machineScope.LinodeMachine.Spec.FirewallID != 0 {
145145
if err := configureFirewall(ctx, machineScope, createConfig, logger); err != nil {
146146
return nil, err
147147
}
@@ -995,13 +995,21 @@ func configurePlacementGroup(ctx context.Context, machineScope *scope.MachineSco
995995

996996
// configureFirewall adds firewall configuration
997997
func configureFirewall(ctx context.Context, machineScope *scope.MachineScope, createConfig *linodego.InstanceCreateOptions, logger logr.Logger) error {
998+
// First check if a direct FirewallID is specified
999+
if machineScope.LinodeMachine.Spec.FirewallID != 0 {
1000+
// Direct FirewallID is provided, use it
1001+
logger.Info("Using direct FirewallID", "firewallID", machineScope.LinodeMachine.Spec.FirewallID)
1002+
createConfig.FirewallID = machineScope.LinodeMachine.Spec.FirewallID
1003+
return nil
1004+
}
1005+
1006+
// If no direct FirewallID, use FirewallRef
9981007
fwID, err := getFirewallID(ctx, machineScope, logger)
9991008
if err != nil {
1000-
logger.Error(err, "Failed to get Firewall config")
1009+
logger.Error(err, "Failed to get Firewall config from reference")
10011010
return err
10021011
}
10031012

10041013
createConfig.FirewallID = fwID
1005-
10061014
return nil
10071015
}

internal/webhook/v1alpha2/linodecluster_webhook.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ func (r *linodeClusterValidator) validateLinodeClusterSpec(ctx context.Context,
141141
})
142142
}
143143

144+
if spec.Network.NodeBalancerFirewallID != nil && spec.NodeBalancerFirewallRef != nil {
145+
errs = append(errs, &field.Error{
146+
Field: "spec.network.nodeBalancerFirewallID/spec.nodeBalancerFirewallRef",
147+
Type: field.ErrorTypeInvalid,
148+
Detail: "Cannot specify both NodeBalancerFirewallID and NodeBalancerFirewallRef",
149+
})
150+
}
151+
144152
if len(errs) == 0 {
145153
return nil
146154
}

internal/webhook/v1alpha2/linodecluster_webhook_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,3 +382,91 @@ func TestValidateVPCIDAndVPCRef(t *testing.T) {
382382
),
383383
)
384384
}
385+
386+
func TestValidateNodeBalancerFirewallIDAndNodeBalancerFirewallRef(t *testing.T) {
387+
t.Parallel()
388+
389+
var (
390+
invalidCluster = &infrav1alpha2.LinodeCluster{
391+
ObjectMeta: metav1.ObjectMeta{
392+
Name: "example",
393+
Namespace: "example",
394+
},
395+
Spec: infrav1alpha2.LinodeClusterSpec{
396+
Region: "us-ord",
397+
Network: infrav1alpha2.NetworkSpec{
398+
NodeBalancerFirewallID: ptr.To(5678),
399+
},
400+
NodeBalancerFirewallRef: &corev1.ObjectReference{
401+
Namespace: "example",
402+
Name: "example",
403+
Kind: "LinodeFirewall",
404+
},
405+
},
406+
}
407+
validClusterWithFirewallID = &infrav1alpha2.LinodeCluster{
408+
ObjectMeta: metav1.ObjectMeta{
409+
Name: "example",
410+
Namespace: "example",
411+
},
412+
Spec: infrav1alpha2.LinodeClusterSpec{
413+
Region: "us-ord",
414+
Network: infrav1alpha2.NetworkSpec{
415+
NodeBalancerFirewallID: ptr.To(5678),
416+
},
417+
},
418+
}
419+
validClusterWithFirewallRef = &infrav1alpha2.LinodeCluster{
420+
ObjectMeta: metav1.ObjectMeta{
421+
Name: "example",
422+
Namespace: "example",
423+
},
424+
Spec: infrav1alpha2.LinodeClusterSpec{
425+
Region: "us-ord",
426+
NodeBalancerFirewallRef: &corev1.ObjectReference{
427+
Namespace: "example",
428+
Name: "example",
429+
Kind: "LinodeFirewall",
430+
},
431+
},
432+
}
433+
validator = &linodeClusterValidator{}
434+
)
435+
436+
NewSuite(t, mock.MockLinodeClient{}).Run(
437+
OneOf(
438+
Path(
439+
Call("valid with NodeBalancerFirewallID", func(ctx context.Context, mck Mock) {
440+
mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
441+
}),
442+
Result("success", func(ctx context.Context, mck Mock) {
443+
errs := validator.validateLinodeClusterSpec(ctx, mck.LinodeClient, validClusterWithFirewallID.Spec, SkipAPIValidation)
444+
require.Empty(t, errs)
445+
}),
446+
),
447+
),
448+
OneOf(
449+
Path(
450+
Call("valid with NodeBalancerFirewallRef", func(ctx context.Context, mck Mock) {
451+
mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
452+
}),
453+
Result("success", func(ctx context.Context, mck Mock) {
454+
errs := validator.validateLinodeClusterSpec(ctx, mck.LinodeClient, validClusterWithFirewallRef.Spec, SkipAPIValidation)
455+
require.Empty(t, errs)
456+
}),
457+
),
458+
),
459+
OneOf(
460+
Path(
461+
Call("both NodeBalancerFirewallID and NodeBalancerFirewallRef set", func(ctx context.Context, mck Mock) {
462+
mck.LinodeClient.EXPECT().GetRegion(gomock.Any(), gomock.Any()).Return(nil, nil).AnyTimes()
463+
}),
464+
Result("error", func(ctx context.Context, mck Mock) {
465+
errs := validator.validateLinodeClusterSpec(ctx, mck.LinodeClient, invalidCluster.Spec, SkipAPIValidation)
466+
require.NotEmpty(t, errs)
467+
require.Contains(t, errs[0].Error(), "Cannot specify both NodeBalancerFirewallID and NodeBalancerFirewallRef")
468+
}),
469+
),
470+
),
471+
)
472+
}

internal/webhook/v1alpha2/linodemachine_webhook.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ func (r *linodeMachineValidator) validateLinodeMachineSpec(ctx context.Context,
150150
})
151151
}
152152

153+
if spec.FirewallID != 0 && spec.FirewallRef != nil {
154+
errs = append(errs, &field.Error{
155+
Field: "spec.firewallID/spec.firewallRef",
156+
Type: field.ErrorTypeInvalid,
157+
Detail: "Cannot specify both FirewallID and FirewallRef",
158+
})
159+
}
160+
153161
if len(errs) == 0 {
154162
return nil
155163
}

0 commit comments

Comments
 (0)