Skip to content

Commit f82a3ac

Browse files
committed
Add support to disable CAPZ components through a manager flag
1 parent 3a9eb5b commit f82a3ac

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
@@ -1245,3 +1245,23 @@ const (
12451245
// AKSAssignedIdentityUserAssigned ...
12461246
AKSAssignedIdentityUserAssigned AKSAssignedIdentity = "UserAssigned"
12471247
)
1248+
1249+
// DisableComponent defines a component to be disabled in CAPZ such as a controller or webhook.
1250+
// +kubebuilder:validation:Enum=DisableASOSecretController;DisableAzureJSONMachineController
1251+
type DisableComponent string
1252+
1253+
// NOTE: when adding a new DisableComponent, please also add it to the ValidDisableableComponents map.
1254+
const (
1255+
// DisableASOSecretController disables the ASOSecretController from being deployed.
1256+
DisableASOSecretController DisableComponent = "DisableASOSecretController"
1257+
1258+
// DisableAzureJSONMachineController disables the AzureJSONMachineController from being deployed.
1259+
DisableAzureJSONMachineController DisableComponent = "DisableAzureJSONMachineController"
1260+
)
1261+
1262+
// ValidDisableableComponents is a map of valid disableable components used to quickly validate whether a component is
1263+
// valid or not.
1264+
var ValidDisableableComponents = map[DisableComponent]struct{}{
1265+
DisableASOSecretController: {},
1266+
DisableAzureJSONMachineController: {},
1267+
}

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
@@ -67,6 +67,7 @@ import (
6767
"sigs.k8s.io/cluster-api-provider-azure/feature"
6868
"sigs.k8s.io/cluster-api-provider-azure/pkg/coalescing"
6969
"sigs.k8s.io/cluster-api-provider-azure/pkg/ot"
70+
"sigs.k8s.io/cluster-api-provider-azure/util/components"
7071
"sigs.k8s.io/cluster-api-provider-azure/util/reconciler"
7172
"sigs.k8s.io/cluster-api-provider-azure/version"
7273
)
@@ -120,6 +121,7 @@ var (
120121
managerOptions = flags.ManagerOptions{}
121122
timeouts reconciler.Timeouts
122123
enableTracing bool
124+
disableControllersOrWebhooks []string
123125
)
124126

125127
// InitFlags initializes all command-line flags.
@@ -266,6 +268,12 @@ func InitFlags(fs *pflag.FlagSet) {
266268
"(Deprecated) 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)",
267269
)
268270

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

271279
feature.MutableGates.AddFlag(fs)
@@ -308,6 +316,16 @@ func main() {
308316
}
309317
}
310318

319+
// Validate valid disable components were passed in the flag
320+
if len(disableControllersOrWebhooks) > 0 {
321+
for _, component := range disableControllersOrWebhooks {
322+
if ok := components.IsValidDisableComponent(component); !ok {
323+
setupLog.Error(fmt.Errorf("invalid disable-controllers-or-webhooks value %s", component), "Invalid argument")
324+
os.Exit(1)
325+
}
326+
}
327+
}
328+
311329
restConfig := ctrl.GetConfigOrDie()
312330
restConfig.UserAgent = "cluster-api-provider-azure-manager"
313331
mgr, err := ctrl.NewManager(restConfig, ctrl.Options{
@@ -420,26 +438,30 @@ func registerControllers(ctx context.Context, mgr manager.Manager) {
420438
os.Exit(1)
421439
}
422440

423-
if err := (&controllers.AzureJSONMachineReconciler{
424-
Client: mgr.GetClient(),
425-
Recorder: mgr.GetEventRecorderFor("azurejsonmachine-reconciler"),
426-
Timeouts: timeouts,
427-
WatchFilterValue: watchFilterValue,
428-
CredentialCache: credCache,
429-
}).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: azureMachineConcurrency, SkipNameValidation: ptr.To(true)}); err != nil {
430-
setupLog.Error(err, "unable to create controller", "controller", "AzureJSONMachine")
431-
os.Exit(1)
441+
if !components.IsComponentDisabled(disableControllersOrWebhooks, infrav1.DisableAzureJSONMachineController) {
442+
if err := (&controllers.AzureJSONMachineReconciler{
443+
Client: mgr.GetClient(),
444+
Recorder: mgr.GetEventRecorderFor("azurejsonmachine-reconciler"),
445+
Timeouts: timeouts,
446+
WatchFilterValue: watchFilterValue,
447+
CredentialCache: credCache,
448+
}).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: azureMachineConcurrency, SkipNameValidation: ptr.To(true)}); err != nil {
449+
setupLog.Error(err, "unable to create controller", "controller", "AzureJSONMachine")
450+
os.Exit(1)
451+
}
432452
}
433453

434-
if err := (&controllers.ASOSecretReconciler{
435-
Client: mgr.GetClient(),
436-
Recorder: mgr.GetEventRecorderFor("asosecret-reconciler"),
437-
Timeouts: timeouts,
438-
WatchFilterValue: watchFilterValue,
439-
CredentialCache: credCache,
440-
}).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: azureClusterConcurrency}); err != nil {
441-
setupLog.Error(err, "unable to create controller", "controller", "ASOSecret")
442-
os.Exit(1)
454+
if !components.IsComponentDisabled(disableControllersOrWebhooks, infrav1.DisableASOSecretController) {
455+
if err := (&controllers.ASOSecretReconciler{
456+
Client: mgr.GetClient(),
457+
Recorder: mgr.GetEventRecorderFor("asosecret-reconciler"),
458+
Timeouts: timeouts,
459+
WatchFilterValue: watchFilterValue,
460+
CredentialCache: credCache,
461+
}).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: azureClusterConcurrency}); err != nil {
462+
setupLog.Error(err, "unable to create controller", "controller", "ASOSecret")
463+
os.Exit(1)
464+
}
443465
}
444466

445467
// 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)