Skip to content

Commit 2e2909c

Browse files
Merge pull request #347 from bryan-cox/CNTRLPLANE-263
OCPBUGS-59888: Add support to disable CAPZ components through a manager flag
2 parents 7dc2245 + d51dd10 commit 2e2909c

File tree

5 files changed

+200
-18
lines changed

5 files changed

+200
-18
lines changed

api/v1beta1/types.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,3 +1220,23 @@ const (
12201220
// AKSAssignedIdentityUserAssigned ...
12211221
AKSAssignedIdentityUserAssigned AKSAssignedIdentity = "UserAssigned"
12221222
)
1223+
1224+
// DisableComponent defines a component to be disabled in CAPZ such as a controller or webhook.
1225+
// +kubebuilder:validation:Enum=DisableASOSecretController;DisableAzureJSONMachineController
1226+
type DisableComponent string
1227+
1228+
// NOTE: when adding a new DisableComponent, please also add it to the ValidDisableableComponents map.
1229+
const (
1230+
// DisableASOSecretController disables the ASOSecretController from being deployed.
1231+
DisableASOSecretController DisableComponent = "DisableASOSecretController"
1232+
1233+
// DisableAzureJSONMachineController disables the AzureJSONMachineController from being deployed.
1234+
DisableAzureJSONMachineController DisableComponent = "DisableAzureJSONMachineController"
1235+
)
1236+
1237+
// ValidDisableableComponents is a map of valid disableable components used to quickly validate whether a component is
1238+
// valid or not.
1239+
var ValidDisableableComponents = map[DisableComponent]struct{}{
1240+
DisableASOSecretController: {},
1241+
DisableAzureJSONMachineController: {},
1242+
}

docs/book/src/self-managed/externally-managed-azure-infrastructure.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,13 @@ If the `AzureCluster` resource includes a "cluster.x-k8s.io/managed-by" annotati
77
This is useful for scenarios where a different persona is managing the cluster infrastructure out-of-band while still wanting to use CAPI for automated machine management.
88

99
You should only use this feature if your cluster infrastructure lifecycle management has constraints that the reference implementation does not support. See [user stories](https://github.com/kubernetes-sigs/cluster-api/blob/10d89ceca938e4d3d94a1d1c2b60515bcdf39829/docs/proposals/20210203-externally-managed-cluster-infrastructure.md#user-stories) for more details.
10+
11+
## Disabling Specific Component Reconciliation
12+
Some controllers/webhooks may not be necessary to run in an externally managed cluster infrastructure scenario. These
13+
controllers/webhooks can be disabled through a flag on the manager called `disable-controllers-or-webhooks`. This flag
14+
accepts a comma separated list of values.
15+
16+
Currently, these are the only accepted values:
17+
1. `DisableASOSecretController` - disables the ASOSecretController from being deployed
18+
2. `DisableAzureJSONMachineController` - disables the AzureJSONMachineController from being deployed
19+

main.go

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import (
6666
"sigs.k8s.io/cluster-api-provider-azure/feature"
6767
"sigs.k8s.io/cluster-api-provider-azure/pkg/coalescing"
6868
"sigs.k8s.io/cluster-api-provider-azure/pkg/ot"
69+
"sigs.k8s.io/cluster-api-provider-azure/util/components"
6970
"sigs.k8s.io/cluster-api-provider-azure/util/reconciler"
7071
"sigs.k8s.io/cluster-api-provider-azure/version"
7172
)
@@ -118,6 +119,7 @@ var (
118119
managerOptions = flags.ManagerOptions{}
119120
timeouts reconciler.Timeouts
120121
enableTracing bool
122+
disableControllersOrWebhooks []string
121123
)
122124

123125
// InitFlags initializes all command-line flags.
@@ -264,6 +266,12 @@ func InitFlags(fs *pflag.FlagSet) {
264266
"Provide fully qualified GVK string to override default kubeadm config watch source, in the form of Kind.version.group (default: KubeadmConfig.v1beta1.bootstrap.cluster.x-k8s.io)",
265267
)
266268

269+
fs.StringSliceVar(&disableControllersOrWebhooks,
270+
"disable-controllers-or-webhooks",
271+
[]string{},
272+
"Comma-separated list of controllers or webhooks to disable. The list can contain the following values: DisableASOSecretController,DisableAzureJSONMachineController",
273+
)
274+
267275
flags.AddManagerOptions(fs, &managerOptions)
268276

269277
feature.MutableGates.AddFlag(fs)
@@ -302,6 +310,16 @@ func main() {
302310
}
303311
}
304312

313+
// Validate valid disable components were passed in the flag
314+
if len(disableControllersOrWebhooks) > 0 {
315+
for _, component := range disableControllersOrWebhooks {
316+
if ok := components.IsValidDisableComponent(component); !ok {
317+
setupLog.Error(fmt.Errorf("invalid disable-controllers-or-webhooks value %s", component), "Invalid argument")
318+
os.Exit(1)
319+
}
320+
}
321+
}
322+
305323
restConfig := ctrl.GetConfigOrDie()
306324
restConfig.UserAgent = "cluster-api-provider-azure-manager"
307325
mgr, err := ctrl.NewManager(restConfig, ctrl.Options{
@@ -414,26 +432,30 @@ func registerControllers(ctx context.Context, mgr manager.Manager) {
414432
os.Exit(1)
415433
}
416434

417-
if err := (&controllers.AzureJSONMachineReconciler{
418-
Client: mgr.GetClient(),
419-
Recorder: mgr.GetEventRecorderFor("azurejsonmachine-reconciler"),
420-
Timeouts: timeouts,
421-
WatchFilterValue: watchFilterValue,
422-
CredentialCache: credCache,
423-
}).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: azureMachineConcurrency, SkipNameValidation: ptr.To(true)}); err != nil {
424-
setupLog.Error(err, "unable to create controller", "controller", "AzureJSONMachine")
425-
os.Exit(1)
435+
if !components.IsComponentDisabled(disableControllersOrWebhooks, infrav1.DisableAzureJSONMachineController) {
436+
if err := (&controllers.AzureJSONMachineReconciler{
437+
Client: mgr.GetClient(),
438+
Recorder: mgr.GetEventRecorderFor("azurejsonmachine-reconciler"),
439+
Timeouts: timeouts,
440+
WatchFilterValue: watchFilterValue,
441+
CredentialCache: credCache,
442+
}).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: azureMachineConcurrency, SkipNameValidation: ptr.To(true)}); err != nil {
443+
setupLog.Error(err, "unable to create controller", "controller", "AzureJSONMachine")
444+
os.Exit(1)
445+
}
426446
}
427447

428-
if err := (&controllers.ASOSecretReconciler{
429-
Client: mgr.GetClient(),
430-
Recorder: mgr.GetEventRecorderFor("asosecret-reconciler"),
431-
Timeouts: timeouts,
432-
WatchFilterValue: watchFilterValue,
433-
CredentialCache: credCache,
434-
}).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: azureClusterConcurrency}); err != nil {
435-
setupLog.Error(err, "unable to create controller", "controller", "ASOSecret")
436-
os.Exit(1)
448+
if !components.IsComponentDisabled(disableControllersOrWebhooks, infrav1.DisableASOSecretController) {
449+
if err := (&controllers.ASOSecretReconciler{
450+
Client: mgr.GetClient(),
451+
Recorder: mgr.GetEventRecorderFor("asosecret-reconciler"),
452+
Timeouts: timeouts,
453+
WatchFilterValue: watchFilterValue,
454+
CredentialCache: credCache,
455+
}).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: azureClusterConcurrency}); err != nil {
456+
setupLog.Error(err, "unable to create controller", "controller", "ASOSecret")
457+
os.Exit(1)
458+
}
437459
}
438460

439461
// just use CAPI MachinePool feature flag rather than create a new one
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package components
18+
19+
import (
20+
"slices"
21+
22+
infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
23+
)
24+
25+
// IsValidDisableComponent validates if the provided value is a valid disable component by checking if the value exists
26+
// in the infrav1.ValidDisableableComponents map.
27+
func IsValidDisableComponent(value string) bool {
28+
_, ok := infrav1.ValidDisableableComponents[infrav1.DisableComponent(value)]
29+
return ok
30+
}
31+
32+
// IsComponentDisabled checks if the provided component is in the list of disabled components.
33+
func IsComponentDisabled(disabledComponents []string, component infrav1.DisableComponent) bool {
34+
return slices.Contains(disabledComponents, string(component))
35+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package components
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/gomega"
23+
24+
infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
25+
)
26+
27+
func TestIsValidDisableComponent(t *testing.T) {
28+
g := NewWithT(t)
29+
30+
testCases := []struct {
31+
name string
32+
value string
33+
expected bool
34+
}{
35+
{
36+
name: "Valid component",
37+
value: string(infrav1.DisableASOSecretController),
38+
expected: true,
39+
},
40+
{
41+
name: "Invalid component",
42+
value: "InvalidComponent",
43+
expected: false,
44+
},
45+
{
46+
name: "Empty string",
47+
value: "",
48+
expected: false,
49+
},
50+
}
51+
52+
for _, tc := range testCases {
53+
t.Run(tc.name, func(t *testing.T) {
54+
result := IsValidDisableComponent(tc.value)
55+
g.Expect(result).To(Equal(tc.expected))
56+
})
57+
}
58+
}
59+
60+
func TestIsComponentDisabled(t *testing.T) {
61+
g := NewGomegaWithT(t)
62+
63+
testCases := []struct {
64+
name string
65+
disabledComponents []string
66+
component infrav1.DisableComponent
67+
expectedResult bool
68+
}{
69+
{
70+
name: "When DisableASOSecretController is in the list, expect true",
71+
disabledComponents: []string{"DisableASOSecretController", "component2"},
72+
component: infrav1.DisableASOSecretController,
73+
expectedResult: true,
74+
},
75+
{
76+
name: "When DisableASOSecretController is not in the list, expect false",
77+
disabledComponents: []string{"component", "component2"},
78+
component: infrav1.DisableASOSecretController,
79+
expectedResult: false,
80+
},
81+
{
82+
name: "When the list is empty, expect false",
83+
disabledComponents: []string{},
84+
component: infrav1.DisableComponent("component"),
85+
expectedResult: false,
86+
},
87+
}
88+
89+
for _, tc := range testCases {
90+
t.Run(tc.name, func(t *testing.T) {
91+
result := IsComponentDisabled(tc.disabledComponents, tc.component)
92+
g.Expect(result).To(Equal(tc.expectedResult))
93+
})
94+
}
95+
}

0 commit comments

Comments
 (0)