diff --git a/fleetconfig-controller/api/v1alpha1/fleetconfig_types.go b/fleetconfig-controller/api/v1alpha1/fleetconfig_types.go index 7893e268..3f0c265c 100644 --- a/fleetconfig-controller/api/v1alpha1/fleetconfig_types.go +++ b/fleetconfig-controller/api/v1alpha1/fleetconfig_types.go @@ -44,6 +44,9 @@ type FleetConfigSpec struct { // +optional AddOnConfigs []AddOnConfig `json:"addOnConfigs,omitempty"` + // +optional + HubAddOns []HubAddOn `json:"hubAddOns,omitempty"` + // Timeout is the timeout in seconds for all clusteradm operations, including init, accept, join, upgrade, etc. // +kubebuilder:default:=300 // +optional @@ -417,7 +420,7 @@ func (s *Spoke) conditionName(c int) string { // AddOn enables add-on installation on the cluster. type AddOn struct { - // The name of the add-on being enabled. Must match one of the default or manually configured add-on names. + // The name of the add-on being enabled. Must match one of the AddOnConfigs or HubAddOns names. // +required ConfigName string `json:"configName"` @@ -767,3 +770,20 @@ func (m *FleetConfig) IsUnjoined(spoke Spoke, joinedSpoke JoinedSpoke) bool { // if both exist and are true, compare timestamps return unjoinedC.LastTransitionTime.After(joinedC.LastTransitionTime.Time) } + +// HubAddOn is the configuration for enabling a built-in AddOn. +type HubAddOn struct { + // Name is the name of the HubAddOn. + // +kubebuilder:validation:Enum=argocd;governance-policy-framework + // +required + Name string `json:"name"` + + // The namespace to install the add-on in. If left empty, installs into the "open-cluster-management-addon" namespace. + // +optional + InstallNamespace string `json:"installNamespace,omitempty"` + + // Whether or not the selected namespace should be created. If left empty, defaults to false. + // +kubebuilder:default:=false + // +optional + CreateNamespace bool `json:"createNamespace,omitempty"` +} diff --git a/fleetconfig-controller/api/v1alpha1/fleetconfig_webhook.go b/fleetconfig-controller/api/v1alpha1/fleetconfig_webhook.go index e515b77b..9899f15d 100644 --- a/fleetconfig-controller/api/v1alpha1/fleetconfig_webhook.go +++ b/fleetconfig-controller/api/v1alpha1/fleetconfig_webhook.go @@ -132,6 +132,7 @@ func (v *FleetConfigCustomValidator) ValidateCreate(ctx context.Context, obj run allErrs = append(allErrs, validateAddonConfigs(ctx, v.client, nil, fc)...) allErrs = append(allErrs, validateAddons(fc)...) + allErrs = append(allErrs, validateHubAddons(ctx, nil, fc)...) if len(allErrs) > 0 { return warnings, errors.NewInvalid(GroupKind, fc.Name, allErrs) } @@ -168,6 +169,7 @@ func (v *FleetConfigCustomValidator) ValidateUpdate(ctx context.Context, oldObj, errs := validateAddonConfigs(ctx, v.client, oldFc, fc) errs = append(errs, validateAddons(fc)...) + errs = append(errs, validateHubAddons(ctx, oldFc, fc)...) if len(errs) > 0 { return nil, errors.NewInvalid(GroupKind, fc.Name, errs) } diff --git a/fleetconfig-controller/api/v1alpha1/validation.go b/fleetconfig-controller/api/v1alpha1/validation.go index 43b28c9c..bb04e315 100644 --- a/fleetconfig-controller/api/v1alpha1/validation.go +++ b/fleetconfig-controller/api/v1alpha1/validation.go @@ -141,15 +141,18 @@ func validateAddonConfigs(ctx context.Context, client client.Client, oldObject, mcAddOns, err := getManagedClusterAddOns(ctx) if err != nil { errs = append(errs, field.InternalError(field.NewPath("addOnConfigs"), err)) - } else { - // Check if any removed addon configs are still in use - for _, removedConfig := range removedAddOnConfigs { - if isAddonConfigInUse(mcAddOns, removedConfig) { - errs = append(errs, field.Invalid(field.NewPath("addOnConfigs"), removedConfig, - fmt.Sprintf("cannot remove addon config %s as it is still in use by managedclusteraddons", removedConfig))) - } + return errs + } + var inUseAddOnConfigs []string + for _, removedConfig := range removedAddOnConfigs { + if isAddondEnabled(mcAddOns, removedConfig) { + inUseAddOnConfigs = append(inUseAddOnConfigs, removedConfig) } } + if len(inUseAddOnConfigs) > 0 { + errs = append(errs, field.Invalid(field.NewPath("addOnConfigs"), inUseAddOnConfigs, + fmt.Sprintf("cannot remove addOnConfigs %v as they are still in use by managedclusteraddons", inUseAddOnConfigs))) + } } } @@ -163,6 +166,9 @@ func validateAddons(newObject *FleetConfig) field.ErrorList { for _, ca := range newObject.Spec.AddOnConfigs { configuredAddons[ca.Name] = true } + for _, ha := range newObject.Spec.HubAddOns { + configuredAddons[ha.Name] = true + } for i, s := range newObject.Spec.Spokes { for j, a := range s.AddOns { if !configuredAddons[a.ConfigName] { @@ -174,6 +180,66 @@ func validateAddons(newObject *FleetConfig) field.ErrorList { return errs } +// validateHubAddons validates HubAddOn configurations and usage +func validateHubAddons(ctx context.Context, oldObject, newObject *FleetConfig) field.ErrorList { + errs := field.ErrorList{} + + // Check for name clashes between HubAddOns and AddOnConfigs + addOnConfigNames := make(map[string]struct{}) + for _, ac := range newObject.Spec.AddOnConfigs { + addOnConfigNames[ac.Name] = struct{}{} + } + + for i, ha := range newObject.Spec.HubAddOns { + if _, found := addOnConfigNames[ha.Name]; found { + errs = append(errs, field.Invalid(field.NewPath("hubAddOn").Index(i), ha.Name, + fmt.Sprintf("hubAddOn name %s clashes with an existing addOnConfig name", ha.Name))) + } + } + + // Check if any removed hub addons are still in use by managed cluster addons + if oldObject != nil { + oldHubAddOns := make(map[string]struct{}) + for _, ha := range oldObject.Spec.HubAddOns { + oldHubAddOns[ha.Name] = struct{}{} + } + + newHubAddOns := make(map[string]struct{}) + for _, ha := range newObject.Spec.HubAddOns { + newHubAddOns[ha.Name] = struct{}{} + } + + removedHubAddOns := make([]string, 0) + for name := range oldHubAddOns { + if _, found := newHubAddOns[name]; !found { + removedHubAddOns = append(removedHubAddOns, name) + } + } + + // Check if any removed hub addons are still in use by managed cluster addons + if len(removedHubAddOns) > 0 { + mcAddOns, err := getManagedClusterAddOns(ctx) + if err != nil { + errs = append(errs, field.InternalError(field.NewPath("hubAddOn"), err)) + return errs + } + var inUseHubAddOns []string + for _, removedHubAddOn := range removedHubAddOns { + if isAddondEnabled(mcAddOns, removedHubAddOn) { + inUseHubAddOns = append(inUseHubAddOns, removedHubAddOn) + } + } + if len(inUseHubAddOns) > 0 { + errs = append(errs, field.Invalid(field.NewPath("hubAddOn"), inUseHubAddOns, + fmt.Sprintf("cannot remove hubAddOns %v as they are still in use by managedclusteraddons", inUseHubAddOns))) + } + + } + } + + return errs +} + // getManagedClusterAddOns lists all ManagedClusterAddOns in all namespaces. func getManagedClusterAddOns(ctx context.Context) ([]addonv1alpha1.ManagedClusterAddOn, error) { restConfig, err := ctrl.GetConfig() @@ -192,10 +258,10 @@ func getManagedClusterAddOns(ctx context.Context) ([]addonv1alpha1.ManagedCluste } // isAddonConfigInUse checks if a removed addon config is still referenced by any ManagedClusterAddOn. -func isAddonConfigInUse(mcAddOns []addonv1alpha1.ManagedClusterAddOn, removedConfig string) bool { +func isAddondEnabled(mcAddOns []addonv1alpha1.ManagedClusterAddOn, removedAddon string) bool { for _, mcao := range mcAddOns { for _, cr := range mcao.Status.ConfigReferences { - if cr.DesiredConfig.Name == removedConfig { + if cr.DesiredConfig.Name == removedAddon { return true } } diff --git a/fleetconfig-controller/api/v1alpha1/zz_generated.deepcopy.go b/fleetconfig-controller/api/v1alpha1/zz_generated.deepcopy.go index e4c4099f..b8fc6b70 100644 --- a/fleetconfig-controller/api/v1alpha1/zz_generated.deepcopy.go +++ b/fleetconfig-controller/api/v1alpha1/zz_generated.deepcopy.go @@ -185,6 +185,11 @@ func (in *FleetConfigSpec) DeepCopyInto(out *FleetConfigSpec) { *out = make([]AddOnConfig, len(*in)) copy(*out, *in) } + if in.HubAddOns != nil { + in, out := &in.HubAddOns, &out.HubAddOns + *out = make([]HubAddOn, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FleetConfigSpec. @@ -287,6 +292,21 @@ func (in *Hub) DeepCopy() *Hub { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HubAddOn) DeepCopyInto(out *HubAddOn) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HubAddOn. +func (in *HubAddOn) DeepCopy() *HubAddOn { + if in == nil { + return nil + } + out := new(HubAddOn) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JoinedSpoke) DeepCopyInto(out *JoinedSpoke) { *out = *in diff --git a/fleetconfig-controller/charts/fleetconfig-controller/crds/fleetconfig.open-cluster-management.io-crds.yaml b/fleetconfig-controller/charts/fleetconfig-controller/crds/fleetconfig.open-cluster-management.io-crds.yaml index 6cd73a1a..9be2604d 100644 --- a/fleetconfig-controller/charts/fleetconfig-controller/crds/fleetconfig.open-cluster-management.io-crds.yaml +++ b/fleetconfig-controller/charts/fleetconfig-controller/crds/fleetconfig.open-cluster-management.io-crds.yaml @@ -273,6 +273,30 @@ spec: required: - kubeconfig type: object + hubAddOns: + items: + description: HubAddOn is the configuration for enabling a built-in + AddOn. + properties: + createNamespace: + default: false + description: Whether or not the selected namespace should be + created. If left empty, defaults to false. + type: boolean + installNamespace: + description: The namespace to install the add-on in. If left + empty, installs into the "open-cluster-management-addon" namespace. + type: string + name: + description: Name is the name of the HubAddOn. + enum: + - argocd + - governance-policy-framework + type: string + required: + - name + type: object + type: array logVerbosity: default: 0 description: LogVerbosity is the verbosity of the logs. @@ -336,8 +360,7 @@ spec: type: object configName: description: The name of the add-on being enabled. Must - match one of the default or manually configured add-on - names. + match one of the AddOnConfigs or HubAddOns names. type: string installNamespace: description: The namespace to install the add-on in. If diff --git a/fleetconfig-controller/config/crd/bases/fleetconfig.open-cluster-management.io_fleetconfigs.yaml b/fleetconfig-controller/config/crd/bases/fleetconfig.open-cluster-management.io_fleetconfigs.yaml index 5334805b..f0465c3d 100644 --- a/fleetconfig-controller/config/crd/bases/fleetconfig.open-cluster-management.io_fleetconfigs.yaml +++ b/fleetconfig-controller/config/crd/bases/fleetconfig.open-cluster-management.io_fleetconfigs.yaml @@ -263,6 +263,30 @@ spec: required: - kubeconfig type: object + hubAddOns: + items: + description: HubAddOn is the configuration for enabling a built-in + AddOn. + properties: + createNamespace: + default: false + description: Whether or not the selected namespace should be + created. If left empty, defaults to false. + type: boolean + installNamespace: + description: The namespace to install the add-on in. If left + empty, installs into the "open-cluster-management-addon" namespace. + type: string + name: + description: Name is the name of the HubAddOn. + enum: + - argocd + - governance-policy-framework + type: string + required: + - name + type: object + type: array logVerbosity: default: 0 description: LogVerbosity is the verbosity of the logs. @@ -326,8 +350,7 @@ spec: type: object configName: description: The name of the add-on being enabled. Must - match one of the default or manually configured add-on - names. + match one of the AddOnConfigs or HubAddOns names. type: string installNamespace: description: The namespace to install the add-on in. If