diff --git a/api/v1beta1/types.go b/api/v1beta1/types.go index 93bed4fc707..4a5c70f9d84 100644 --- a/api/v1beta1/types.go +++ b/api/v1beta1/types.go @@ -1245,3 +1245,23 @@ const ( // AKSAssignedIdentityUserAssigned ... AKSAssignedIdentityUserAssigned AKSAssignedIdentity = "UserAssigned" ) + +// DisableComponent defines a component to be disabled in CAPZ such as a controller or webhook. +// +kubebuilder:validation:Enum=DisableASOSecretController;DisableAzureJSONMachineController +type DisableComponent string + +// NOTE: when adding a new DisableComponent, please also add it to the ValidDisableableComponents map. +const ( + // DisableASOController disables the ASOSecretController from being deployed. + DisableASOSecretController DisableComponent = "DisableASOSecretController" + + // DisableAzureJSONMachineController disables the AzureJSONMachineController from being deployed. + DisableAzureJSONMachineController DisableComponent = "DisableAzureJSONMachineController" +) + +// ValidDisableableComponents is a map of valid disableable components used to quickly validate whether a component is +// valid or not. +var ValidDisableableComponents = map[DisableComponent]struct{}{ + DisableASOSecretController: {}, + DisableAzureJSONMachineController: {}, +} diff --git a/docs/book/src/self-managed/externally-managed-azure-infrastructure.md b/docs/book/src/self-managed/externally-managed-azure-infrastructure.md index 923230ba161..61ba3a89eaa 100644 --- a/docs/book/src/self-managed/externally-managed-azure-infrastructure.md +++ b/docs/book/src/self-managed/externally-managed-azure-infrastructure.md @@ -7,3 +7,13 @@ If the `AzureCluster` resource includes a "cluster.x-k8s.io/managed-by" annotati 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. 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. + +## Disabling Specific Component Reconciliation +Some controllers/webhooks may not be necessary to run in an externally managed cluster infrastructure scenario. These +controllers/webhooks can be disabled through a flag on the manager called `disable-controllers-or-webhooks`. This flag +accepts a comma separated list of values. + +Currently, these are the only accepted values: +1. `DisableASOSecretController` - disables the ASOSecretController from being deployed +2. `DisableAzureJSONMachineController` - disables the AzureJSONMachineController from being deployed + diff --git a/main.go b/main.go index 343c0e8b0f2..d2d68603af3 100644 --- a/main.go +++ b/main.go @@ -67,6 +67,7 @@ import ( "sigs.k8s.io/cluster-api-provider-azure/feature" "sigs.k8s.io/cluster-api-provider-azure/pkg/coalescing" "sigs.k8s.io/cluster-api-provider-azure/pkg/ot" + "sigs.k8s.io/cluster-api-provider-azure/util/components" "sigs.k8s.io/cluster-api-provider-azure/util/reconciler" "sigs.k8s.io/cluster-api-provider-azure/version" ) @@ -120,6 +121,7 @@ var ( managerOptions = flags.ManagerOptions{} timeouts reconciler.Timeouts enableTracing bool + disableControllersOrWebhooks []string ) // InitFlags initializes all command-line flags. @@ -266,6 +268,12 @@ func InitFlags(fs *pflag.FlagSet) { "(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)", ) + fs.StringSliceVar(&disableControllersOrWebhooks, + "disable-controllers-or-webhooks", + []string{}, + "Comma-separated list of controllers or webhooks to disable. The list can contain the following values: DisableASOSecretController,DisableAzureJSONMachineController", + ) + flags.AddManagerOptions(fs, &managerOptions) feature.MutableGates.AddFlag(fs) @@ -308,6 +316,16 @@ func main() { } } + // Validate valid disable components were passed in the flag + if len(disableControllersOrWebhooks) > 0 { + for _, component := range disableControllersOrWebhooks { + if ok := components.IsValidDisableComponent(component); !ok { + setupLog.Error(fmt.Errorf("invalid disable-controllers-or-webhooks value %s", component), "Invalid argument") + os.Exit(1) + } + } + } + restConfig := ctrl.GetConfigOrDie() restConfig.UserAgent = "cluster-api-provider-azure-manager" mgr, err := ctrl.NewManager(restConfig, ctrl.Options{ @@ -420,26 +438,30 @@ func registerControllers(ctx context.Context, mgr manager.Manager) { os.Exit(1) } - if err := (&controllers.AzureJSONMachineReconciler{ - Client: mgr.GetClient(), - Recorder: mgr.GetEventRecorderFor("azurejsonmachine-reconciler"), - Timeouts: timeouts, - WatchFilterValue: watchFilterValue, - CredentialCache: credCache, - }).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: azureMachineConcurrency, SkipNameValidation: ptr.To(true)}); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "AzureJSONMachine") - os.Exit(1) + if !components.IsComponentDisabled(disableControllersOrWebhooks, infrav1.DisableAzureJSONMachineController) { + if err := (&controllers.AzureJSONMachineReconciler{ + Client: mgr.GetClient(), + Recorder: mgr.GetEventRecorderFor("azurejsonmachine-reconciler"), + Timeouts: timeouts, + WatchFilterValue: watchFilterValue, + CredentialCache: credCache, + }).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: azureMachineConcurrency, SkipNameValidation: ptr.To(true)}); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "AzureJSONMachine") + os.Exit(1) + } } - if err := (&controllers.ASOSecretReconciler{ - Client: mgr.GetClient(), - Recorder: mgr.GetEventRecorderFor("asosecret-reconciler"), - Timeouts: timeouts, - WatchFilterValue: watchFilterValue, - CredentialCache: credCache, - }).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: azureClusterConcurrency}); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "ASOSecret") - os.Exit(1) + if !components.IsComponentDisabled(disableControllersOrWebhooks, infrav1.DisableASOSecretController) { + if err := (&controllers.ASOSecretReconciler{ + Client: mgr.GetClient(), + Recorder: mgr.GetEventRecorderFor("asosecret-reconciler"), + Timeouts: timeouts, + WatchFilterValue: watchFilterValue, + CredentialCache: credCache, + }).SetupWithManager(ctx, mgr, controller.Options{MaxConcurrentReconciles: azureClusterConcurrency}); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ASOSecret") + os.Exit(1) + } } // just use CAPI MachinePool feature flag rather than create a new one diff --git a/util/components/disabled_components.go b/util/components/disabled_components.go new file mode 100644 index 00000000000..f9652463314 --- /dev/null +++ b/util/components/disabled_components.go @@ -0,0 +1,35 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package components + +import ( + "slices" + + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" +) + +// IsValidDisableComponent validates if the provided value is a valid disable component by checking if the value exists +// in the infrav1.ValidDisableableComponents map. +func IsValidDisableComponent(value string) bool { + _, ok := infrav1.ValidDisableableComponents[infrav1.DisableComponent(value)] + return ok +} + +// IsComponentDisabled checks if the provided component is in the list of disabled components. +func IsComponentDisabled(disabledComponents []string, component infrav1.DisableComponent) bool { + return slices.Contains(disabledComponents, string(component)) +} diff --git a/util/components/disabled_components_test.go b/util/components/disabled_components_test.go new file mode 100644 index 00000000000..d49d64d76ef --- /dev/null +++ b/util/components/disabled_components_test.go @@ -0,0 +1,95 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package components + +import ( + "testing" + + . "github.com/onsi/gomega" + + infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" +) + +func TestIsValidDisableComponent(t *testing.T) { + g := NewWithT(t) + + testCases := []struct { + name string + value string + expected bool + }{ + { + name: "Valid component", + value: string(infrav1.DisableASOSecretController), + expected: true, + }, + { + name: "Invalid component", + value: "InvalidComponent", + expected: false, + }, + { + name: "Empty string", + value: "", + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := IsValidDisableComponent(tc.value) + g.Expect(result).To(Equal(tc.expected)) + }) + } +} + +func TestIsComponentDisabled(t *testing.T) { + g := NewGomegaWithT(t) + + testCases := []struct { + name string + disabledComponents []string + component infrav1.DisableComponent + expectedResult bool + }{ + { + name: "When DisableASOSecretController is in the list, expect true", + disabledComponents: []string{"DisableASOSecretController", "component2"}, + component: infrav1.DisableASOSecretController, + expectedResult: true, + }, + { + name: "When DisableASOSecretController is not in the list, expect false", + disabledComponents: []string{"component", "component2"}, + component: infrav1.DisableASOSecretController, + expectedResult: false, + }, + { + name: "When the list is empty, expect false", + disabledComponents: []string{}, + component: infrav1.DisableComponent("component"), + expectedResult: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := IsComponentDisabled(tc.disabledComponents, tc.component) + g.Expect(result).To(Equal(tc.expectedResult)) + }) + } +}