From b18664f2bca96fe205faff40cfe76f80e30aa32e Mon Sep 17 00:00:00 2001 From: zerospiel Date: Fri, 27 Sep 2024 17:25:01 +0200 Subject: [PATCH] Add support for compatibility attributes * Cluster/Service/Provider Templates now have support for compatibility attributes * Managed/Management Clusters now have support to report the compatibility attributes * amends to the templates controller in regards of the API changes * amends to other parts of the code Closes #354 --- api/v1alpha1/clustertemplate_types.go | 101 +++++++++++++- api/v1alpha1/common.go | 89 +++++++++++-- api/v1alpha1/managedcluster_types.go | 5 +- api/v1alpha1/management_types.go | 18 +-- api/v1alpha1/providertemplate_types.go | 101 +++++++++++++- api/v1alpha1/servicetemplate_types.go | 59 +++++++- api/v1alpha1/templates_common.go | 35 +++-- api/v1alpha1/zz_generated.deepcopy.go | 124 +++++++++++++---- cmd/main.go | 1 - go.mod | 2 +- .../controller/managedcluster_controller.go | 5 +- .../managedcluster_controller_test.go | 14 +- internal/controller/management_controller.go | 16 ++- .../controller/management_controller_test.go | 6 +- internal/controller/release_controller.go | 4 +- internal/controller/template_controller.go | 121 +++++++---------- .../controller/template_controller_test.go | 24 ++-- .../templatemanagement_controller.go | 16 +-- internal/telemetry/tracker.go | 6 +- internal/utils/util.go | 38 ------ internal/webhook/managedcluster_webhook.go | 19 ++- .../webhook/managedcluster_webhook_test.go | 30 ++--- .../hmc.mirantis.com_clustertemplates.yaml | 98 +++++++++++--- .../hmc.mirantis.com_managedclusters.yaml | 9 +- .../crds/hmc.mirantis.com_managements.yaml | 50 +++++-- .../hmc.mirantis.com_providertemplates.yaml | 97 +++++++++++--- .../hmc.mirantis.com_servicetemplates.yaml | 8 +- test/objects/management/management.go | 2 +- test/objects/template/template.go | 126 +++++++++++------- 29 files changed, 865 insertions(+), 359 deletions(-) delete mode 100644 internal/utils/util.go diff --git a/api/v1alpha1/clustertemplate_types.go b/api/v1alpha1/clustertemplate_types.go index 2af641b4..e9e1d9fa 100644 --- a/api/v1alpha1/clustertemplate_types.go +++ b/api/v1alpha1/clustertemplate_types.go @@ -15,24 +15,117 @@ package v1alpha1 import ( + "errors" + "fmt" + "strings" + + "github.com/Masterminds/semver/v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // ClusterTemplateSpec defines the desired state of ClusterTemplate type ClusterTemplateSpec struct { - TemplateSpecCommon `json:",inline"` + Helm HelmSpec `json:"helm"` + // Compatible K8S version of the cluster set in the SemVer format. + KubertenesVersion string `json:"k8sVersion,omitempty"` + // Providers represent required/exposed CAPI providers depending on the template type. + // Should be set if not present in the Helm chart metadata. + Providers ProvidersConstrainted `json:"providers,omitempty"` } // ClusterTemplateStatus defines the observed state of ClusterTemplate type ClusterTemplateStatus struct { + // Providers represent required/exposed CAPI providers depending on the template type. + Providers ProvidersConstrainted `json:"providers,omitempty"` + TemplateStatusCommon `json:",inline"` } -func (t *ClusterTemplate) GetSpec() *TemplateSpecCommon { - return &t.Spec.TemplateSpecCommon +// FillStatusWithProviders sets the status of the template with providers +// either from the spec or from the given annotations. +func (t *ClusterTemplate) FillStatusWithProviders(annotations map[string]string) error { //nolint:dupl // no sane approach to fully avoid duplication in terms of types + parseProviders := func(typ providersType) ([]NameConstraint, error) { + var ( + pspec, pstatus []NameConstraint + anno string + ) + switch typ { + case bootstrapProvidersType: + pspec, pstatus = t.Spec.Providers.BootstrapProviders, t.Status.Providers.BootstrapProviders + anno = ChartAnnotationBootstrapProviders + case controlPlaneProvidersType: + pspec, pstatus = t.Spec.Providers.ControlPlaneProviders, t.Status.Providers.ControlPlaneProviders + anno = ChartAnnotationControlPlaneProviders + case infrastructureProvidersType: + pspec, pstatus = t.Spec.Providers.InfrastructureProviders, t.Status.Providers.InfrastructureProviders + anno = ChartAnnotationInfraProviders + } + + if len(pspec) > 0 { + return pstatus, nil + } + + providers := annotations[anno] + if len(providers) == 0 { + return []NameConstraint{}, nil + } + + var ( + constrainted = strings.Split(providers, ",") + merr error + ) + + pstatus = make([]NameConstraint, 0, len(constrainted)) + + for _, v := range constrainted { + nc := strings.SplitN(v, " ", 1) + if len(nc) == 0 { // BCE (bound check elimination) + continue + } + + n := NameConstraint{Name: nc[0]} + if len(nc) < 2 { + pstatus = append(pstatus, n) + continue + } + + c := strings.TrimSpace(nc[1]) + if _, err := semver.NewConstraint(c); err != nil { // validation + merr = errors.Join(merr, fmt.Errorf("failed to parse constraint %s in the %s: %v", c, v, err)) + continue + } + + n.Constraint = c + pstatus = append(pstatus, n) + } + + return pstatus, merr + } + + var err error + t.Status.Providers.BootstrapProviders, err = parseProviders(bootstrapProvidersType) + if err != nil { + return fmt.Errorf("failed to parse bootstrap providers: %v", err) + } + + t.Status.Providers.ControlPlaneProviders, err = parseProviders(controlPlaneProvidersType) + if err != nil { + return fmt.Errorf("failed to parse controlPlane providers: %v", err) + } + + t.Status.Providers.InfrastructureProviders, err = parseProviders(infrastructureProvidersType) + if err != nil { + return fmt.Errorf("failed to parse infrastructure providers: %v", err) + } + + return nil +} + +func (t *ClusterTemplate) GetHelmSpec() *HelmSpec { + return &t.Spec.Helm } -func (t *ClusterTemplate) GetStatus() *TemplateStatusCommon { +func (t *ClusterTemplate) GetCommonStatus() *TemplateStatusCommon { return &t.Status.TemplateStatusCommon } diff --git a/api/v1alpha1/common.go b/api/v1alpha1/common.go index 5701f9c1..c055ac5f 100644 --- a/api/v1alpha1/common.go +++ b/api/v1alpha1/common.go @@ -21,15 +21,53 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -// Providers is a structure holding different types of CAPI providers -type Providers struct { - // InfrastructureProviders is the list of CAPI infrastructure providers - InfrastructureProviders []string `json:"infrastructure,omitempty"` - // BootstrapProviders is the list of CAPI bootstrap providers - BootstrapProviders []string `json:"bootstrap,omitempty"` - // ControlPlaneProviders is the list of CAPI control plane providers - ControlPlaneProviders []string `json:"controlPlane,omitempty"` -} +type ( + // Providers hold different types of CAPI providers. + Providers struct { + // InfrastructureProviders is the list of CAPI infrastructure providers + InfrastructureProviders []string `json:"infrastructure,omitempty"` + // BootstrapProviders is the list of CAPI bootstrap providers + BootstrapProviders []string `json:"bootstrap,omitempty"` + // ControlPlaneProviders is the list of CAPI control plane providers + ControlPlaneProviders []string `json:"controlPlane,omitempty"` + } + + // ProvidersVersioned hold different types of CAPI providers with supported versions. + ProvidersVersioned struct { + // List of CAPI infrastructure providers with the supported version. + InfrastructureProviders []NameVersion `json:"infrastructure,omitempty"` + // List of CAPI bootstrap providers with the supported version. + BootstrapProviders []NameVersion `json:"bootstrap,omitempty"` + // List of CAPI control plane providers with the supported version. + ControlPlaneProviders []NameVersion `json:"controlPlane,omitempty"` + } + + // ProvidersConstrainted hold different types of CAPI providers with constrainted versions. + ProvidersConstrainted struct { + // List of CAPI infrastructure providers with the SemVer constraint. + InfrastructureProviders []NameConstraint `json:"infrastructure,omitempty"` + // List of CAPI bootstrap providers with the SemVer constraint. + BootstrapProviders []NameConstraint `json:"bootstrap,omitempty"` + // List of CAPI control plane providers with the SemVer constraint. + ControlPlaneProviders []NameConstraint `json:"controlPlane,omitempty"` + } + + // NameVersion represents name of the provider with its supported version. + NameVersion struct { + // Name of the provider. + Name string `json:"name,omitempty"` + // Supported exact version in the SemVer format. + Version string `json:"version,omitempty"` + } + + // NameConstraint represents name of the provider with its constrainted version. + NameConstraint struct { + // Name of the provider. + Name string `json:"name,omitempty"` + // Supported versions set via the SemVer constraint. + Constraint string `json:"constraint,omitempty"` + } +) const ( // Provider CAPA @@ -80,3 +118,36 @@ func ExtractReleaseVersion(rawObj client.Object) []string { } return []string{release.Spec.Version} } + +func (c ProvidersConstrainted) BootstrapProvidersNames() []string { + return c.names(bootstrapProvidersType) +} + +func (c ProvidersConstrainted) ControlPlaneProvidersNames() []string { + return c.names(bootstrapProvidersType) +} + +func (c ProvidersConstrainted) InfrastructureProvidersNames() []string { + return c.names(bootstrapProvidersType) +} + +func (c ProvidersConstrainted) names(typ providersType) []string { + f := func(nn []NameConstraint) []string { + res := make([]string, len(nn)) + for i, v := range nn { + res[i] = v.Name + } + return res + } + + switch typ { + case bootstrapProvidersType: + return f(c.BootstrapProviders) + case controlPlaneProvidersType: + return f(c.ControlPlaneProviders) + case infrastructureProvidersType: + return f(c.InfrastructureProviders) + default: + return []string{} + } +} diff --git a/api/v1alpha1/managedcluster_types.go b/api/v1alpha1/managedcluster_types.go index 17f8a552..be07d25c 100644 --- a/api/v1alpha1/managedcluster_types.go +++ b/api/v1alpha1/managedcluster_types.go @@ -80,6 +80,9 @@ type ManagedClusterSpec struct { // ManagedClusterStatus defines the observed state of ManagedCluster type ManagedClusterStatus struct { + // Currently compatible K8S version of the cluster. Being set only if + // the corresponding ClusterTemplate provided it in the spec. + KubertenesVersion string `json:"k8sVersion,omitempty"` // Conditions contains details for the current state of the ManagedCluster Conditions []metav1.Condition `json:"conditions,omitempty"` // ObservedGeneration is the last observed generation. @@ -88,7 +91,7 @@ type ManagedClusterStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status -// +kubebuilder:resource:shortName=hmc-deploy;deploy +// +kubebuilder:resource:shortName=hmcmc;mc // +kubebuilder:printcolumn:name="ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="Ready",priority=0 // +kubebuilder:printcolumn:name="status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="Status",priority=0 // +kubebuilder:printcolumn:name="dryRun",type="string",JSONPath=".spec.dryRun",description="Dry Run",priority=1 diff --git a/api/v1alpha1/management_types.go b/api/v1alpha1/management_types.go index f2b73174..a62bd6a0 100644 --- a/api/v1alpha1/management_types.go +++ b/api/v1alpha1/management_types.go @@ -75,35 +75,35 @@ func (in *Component) HelmValues() (values map[string]any, err error) { return values, err } -func (m *ManagementSpec) SetProvidersDefaults() error { - m.Providers = []Provider{ +func GetDefaultProviders() []Provider { + return []Provider{ {Name: ProviderK0smotronName}, {Name: ProviderCAPAName}, {Name: ProviderAzureName}, {Name: ProviderVSphereName}, {Name: ProviderSveltosName}, } - return nil } // ManagementStatus defines the observed state of Management type ManagementStatus struct { - // ObservedGeneration is the last observed generation. - ObservedGeneration int64 `json:"observedGeneration,omitempty"` - // AvailableProviders holds all CAPI providers available on the Management cluster. - AvailableProviders Providers `json:"availableProviders,omitempty"` // Components indicates the status of installed HMC components and CAPI providers. Components map[string]ComponentStatus `json:"components,omitempty"` + // AvailableProviders holds all CAPI providers available along with + // their versions if specified in ProviderTemplates on the Management cluster. + AvailableProviders ProvidersVersioned `json:"availableProviders,omitempty"` + // ObservedGeneration is the last observed generation. + ObservedGeneration int64 `json:"observedGeneration,omitempty"` } // ComponentStatus is the status of Management component installation type ComponentStatus struct { // Template is the name of the Template associated with this component. Template string `json:"template,omitempty"` - // Success represents if a component installation was successful - Success bool `json:"success,omitempty"` // Error stores as error message in case of failed installation Error string `json:"error,omitempty"` + // Success represents if a component installation was successful + Success bool `json:"success,omitempty"` } // +kubebuilder:object:root=true diff --git a/api/v1alpha1/providertemplate_types.go b/api/v1alpha1/providertemplate_types.go index b0e30a97..7df06180 100644 --- a/api/v1alpha1/providertemplate_types.go +++ b/api/v1alpha1/providertemplate_types.go @@ -15,24 +15,117 @@ package v1alpha1 import ( + "errors" + "fmt" + "strings" + + "github.com/Masterminds/semver/v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // ProviderTemplateSpec defines the desired state of ProviderTemplate type ProviderTemplateSpec struct { - TemplateSpecCommon `json:",inline"` + Helm HelmSpec `json:"helm"` + // Compatible CAPI provider version set in the SemVer format. + CAPIVersion string `json:"capiVersion,omitempty"` + // Providers represent required/exposed CAPI providers depending on the template type. + // Should be set if not present in the Helm chart metadata. + Providers ProvidersVersioned `json:"providers,omitempty"` } // ProviderTemplateStatus defines the observed state of ProviderTemplate type ProviderTemplateStatus struct { + // Providers represent required/exposed CAPI providers depending on the template type. + Providers ProvidersVersioned `json:"providers,omitempty"` + TemplateStatusCommon `json:",inline"` } -func (t *ProviderTemplate) GetSpec() *TemplateSpecCommon { - return &t.Spec.TemplateSpecCommon +// FillStatusWithProviders sets the status of the template with providers +// either from the spec or from the given annotations. +func (t *ProviderTemplate) FillStatusWithProviders(annotations map[string]string) error { + parseProviders := func(typ providersType) ([]NameVersion, error) { + var ( + pspec, pstatus []NameVersion + anno string + ) + switch typ { + case bootstrapProvidersType: + pspec, pstatus = t.Spec.Providers.BootstrapProviders, t.Status.Providers.BootstrapProviders + anno = ChartAnnotationBootstrapProviders + case controlPlaneProvidersType: + pspec, pstatus = t.Spec.Providers.ControlPlaneProviders, t.Status.Providers.ControlPlaneProviders + anno = ChartAnnotationControlPlaneProviders + case infrastructureProvidersType: + pspec, pstatus = t.Spec.Providers.InfrastructureProviders, t.Status.Providers.InfrastructureProviders + anno = ChartAnnotationInfraProviders + } + + if len(pspec) > 0 { + return pstatus, nil + } + + providers := annotations[anno] + if len(providers) == 0 { + return []NameVersion{}, nil + } + + var ( + versioned = strings.Split(providers, ",") + merr error + ) + + pstatus = make([]NameVersion, 0, len(versioned)) + + for _, v := range versioned { + nver := strings.SplitN(v, " ", 1) + if len(nver) == 0 { // BCE (bound check elimination) + continue + } + + n := NameVersion{Name: nver[0]} + if len(nver) < 2 { + pstatus = append(pstatus, n) + continue + } + + ver := strings.TrimSpace(nver[1]) + if _, err := semver.NewVersion(ver); err != nil { // validation + merr = errors.Join(merr, fmt.Errorf("failed to parse version %s in the %s: %v", ver, v, err)) + continue + } + + n.Version = ver + pstatus = append(pstatus, n) + } + + return pstatus, merr + } + + var err error + t.Status.Providers.BootstrapProviders, err = parseProviders(bootstrapProvidersType) + if err != nil { + return fmt.Errorf("failed to parse bootstrap providers: %v", err) + } + + t.Status.Providers.ControlPlaneProviders, err = parseProviders(controlPlaneProvidersType) + if err != nil { + return fmt.Errorf("failed to parse controlPlane providers: %v", err) + } + + t.Status.Providers.InfrastructureProviders, err = parseProviders(infrastructureProvidersType) + if err != nil { + return fmt.Errorf("failed to parse infrastructure providers: %v", err) + } + + return nil +} + +func (t *ProviderTemplate) GetHelmSpec() *HelmSpec { + return &t.Spec.Helm } -func (t *ProviderTemplate) GetStatus() *TemplateStatusCommon { +func (t *ProviderTemplate) GetCommonStatus() *TemplateStatusCommon { return &t.Status.TemplateStatusCommon } diff --git a/api/v1alpha1/servicetemplate_types.go b/api/v1alpha1/servicetemplate_types.go index e9d083d1..a390783c 100644 --- a/api/v1alpha1/servicetemplate_types.go +++ b/api/v1alpha1/servicetemplate_types.go @@ -15,24 +15,75 @@ package v1alpha1 import ( + "strings" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // ServiceTemplateSpec defines the desired state of ServiceTemplate type ServiceTemplateSpec struct { - TemplateSpecCommon `json:",inline"` + Helm HelmSpec `json:"helm"` + // Constraint describing compatible K8S versions of the cluster set in the SemVer format. + KubertenesConstraint string `json:"k8sConstraint,omitempty"` + // Providers represent required/exposed CAPI providers depending on the template type. + // Should be set if not present in the Helm chart metadata. + Providers Providers `json:"providers,omitempty"` } // ServiceTemplateStatus defines the observed state of ServiceTemplate type ServiceTemplateStatus struct { + // Providers represent required/exposed CAPI providers depending on the template type. + Providers Providers `json:"providers,omitempty"` + TemplateStatusCommon `json:",inline"` } -func (t *ServiceTemplate) GetSpec() *TemplateSpecCommon { - return &t.Spec.TemplateSpecCommon +// FillStatusWithProviders sets the status of the template with providers +// either from the spec or from the given annotations. +// +// The return parameter is noop and is always nil. +func (t *ServiceTemplate) FillStatusWithProviders(annotations map[string]string) error { + parseProviders := func(typ providersType) []string { + var ( + pspec, pstatus []string + anno string + ) + switch typ { + case bootstrapProvidersType: + pspec, pstatus = t.Spec.Providers.BootstrapProviders, t.Status.Providers.BootstrapProviders + anno = ChartAnnotationBootstrapProviders + case controlPlaneProvidersType: + pspec, pstatus = t.Spec.Providers.ControlPlaneProviders, t.Status.Providers.ControlPlaneProviders + anno = ChartAnnotationControlPlaneProviders + case infrastructureProvidersType: + pspec, pstatus = t.Spec.Providers.InfrastructureProviders, t.Status.Providers.InfrastructureProviders + anno = ChartAnnotationInfraProviders + } + + if len(pspec) > 0 { + return pstatus + } + + providers := annotations[anno] + if len(providers) == 0 { + return []string{} + } + + return strings.Split(providers, ",") + } + + t.Status.Providers.BootstrapProviders = parseProviders(bootstrapProvidersType) + t.Status.Providers.ControlPlaneProviders = parseProviders(controlPlaneProvidersType) + t.Status.Providers.InfrastructureProviders = parseProviders(infrastructureProvidersType) + + return nil +} + +func (t *ServiceTemplate) GetHelmSpec() *HelmSpec { + return &t.Spec.Helm } -func (t *ServiceTemplate) GetStatus() *TemplateStatusCommon { +func (t *ServiceTemplate) GetCommonStatus() *TemplateStatusCommon { return &t.Status.TemplateStatusCommon } diff --git a/api/v1alpha1/templates_common.go b/api/v1alpha1/templates_common.go index 98bd9ccf..30916429 100644 --- a/api/v1alpha1/templates_common.go +++ b/api/v1alpha1/templates_common.go @@ -28,15 +28,6 @@ const ( ChartAnnotationControlPlaneProviders = "hmc.mirantis.com/control-plane-providers" ) -// TemplateSpecCommon is a Template configuration common for all Template types -type TemplateSpecCommon struct { - // Helm holds a reference to a Helm chart representing the HMC template - Helm HelmSpec `json:"helm"` - // Providers represent required/exposed CAPI providers depending on the template type. - // Should be set if not present in the Helm chart metadata. - Providers Providers `json:"providers,omitempty"` -} - // +kubebuilder:validation:XValidation:rule="(has(self.chartName) && !has(self.chartRef)) || (!has(self.chartName) && has(self.chartRef))", message="either chartName or chartRef must be set" // HelmSpec references a Helm chart representing the HMC template @@ -50,19 +41,27 @@ type HelmSpec struct { ChartVersion string `json:"chartVersion,omitempty"` } +func (s *HelmSpec) String() string { + if s.ChartRef != nil { + return s.ChartRef.Namespace + "/" + s.ChartRef.Name + ", Kind=" + s.ChartRef.Kind + } + + return s.ChartName + ": " + s.ChartVersion +} + // TemplateStatusCommon defines the observed state of Template common for all Template types type TemplateStatusCommon struct { - TemplateValidationStatus `json:",inline"` - // Description contains information about the template. - Description string `json:"description,omitempty"` // Config demonstrates available parameters for template customization, // that can be used when creating ManagedCluster objects. Config *apiextensionsv1.JSON `json:"config,omitempty"` // ChartRef is a reference to a source controller resource containing the // Helm chart representing the template. ChartRef *helmcontrollerv2.CrossNamespaceSourceReference `json:"chartRef,omitempty"` - // Providers represent required/exposed CAPI providers depending on the template type. - Providers Providers `json:"providers,omitempty"` + // Description contains information about the template. + Description string `json:"description,omitempty"` + + TemplateValidationStatus `json:",inline"` + // ObservedGeneration is the last observed generation. ObservedGeneration int64 `json:"observedGeneration,omitempty"` } @@ -73,3 +72,11 @@ type TemplateValidationStatus struct { // Valid indicates whether the template passed validation or not. Valid bool `json:"valid"` } + +type providersType int + +const ( + bootstrapProvidersType providersType = iota + controlPlaneProvidersType + infrastructureProvidersType +) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index a94afc48..b14070cc 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -187,7 +187,8 @@ func (in *ClusterTemplateList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterTemplateSpec) DeepCopyInto(out *ClusterTemplateSpec) { *out = *in - in.TemplateSpecCommon.DeepCopyInto(&out.TemplateSpecCommon) + in.Helm.DeepCopyInto(&out.Helm) + in.Providers.DeepCopyInto(&out.Providers) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterTemplateSpec. @@ -203,6 +204,7 @@ func (in *ClusterTemplateSpec) DeepCopy() *ClusterTemplateSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterTemplateStatus) DeepCopyInto(out *ClusterTemplateStatus) { *out = *in + in.Providers.DeepCopyInto(&out.Providers) in.TemplateStatusCommon.DeepCopyInto(&out.TemplateStatusCommon) } @@ -587,7 +589,6 @@ func (in *ManagementSpec) DeepCopy() *ManagementSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ManagementStatus) DeepCopyInto(out *ManagementStatus) { *out = *in - in.AvailableProviders.DeepCopyInto(&out.AvailableProviders) if in.Components != nil { in, out := &in.Components, &out.Components *out = make(map[string]ComponentStatus, len(*in)) @@ -595,6 +596,7 @@ func (in *ManagementStatus) DeepCopyInto(out *ManagementStatus) { (*out)[key] = val } } + in.AvailableProviders.DeepCopyInto(&out.AvailableProviders) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagementStatus. @@ -607,6 +609,36 @@ func (in *ManagementStatus) DeepCopy() *ManagementStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NameConstraint) DeepCopyInto(out *NameConstraint) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NameConstraint. +func (in *NameConstraint) DeepCopy() *NameConstraint { + if in == nil { + return nil + } + out := new(NameConstraint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NameVersion) DeepCopyInto(out *NameVersion) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NameVersion. +func (in *NameVersion) DeepCopy() *NameVersion { + if in == nil { + return nil + } + out := new(NameVersion) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NamedProviderTemplate) DeepCopyInto(out *NamedProviderTemplate) { *out = *in @@ -701,7 +733,8 @@ func (in *ProviderTemplateList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProviderTemplateSpec) DeepCopyInto(out *ProviderTemplateSpec) { *out = *in - in.TemplateSpecCommon.DeepCopyInto(&out.TemplateSpecCommon) + in.Helm.DeepCopyInto(&out.Helm) + in.Providers.DeepCopyInto(&out.Providers) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderTemplateSpec. @@ -717,6 +750,7 @@ func (in *ProviderTemplateSpec) DeepCopy() *ProviderTemplateSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ProviderTemplateStatus) DeepCopyInto(out *ProviderTemplateStatus) { *out = *in + in.Providers.DeepCopyInto(&out.Providers) in.TemplateStatusCommon.DeepCopyInto(&out.TemplateStatusCommon) } @@ -760,6 +794,66 @@ func (in *Providers) DeepCopy() *Providers { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProvidersConstrainted) DeepCopyInto(out *ProvidersConstrainted) { + *out = *in + if in.InfrastructureProviders != nil { + in, out := &in.InfrastructureProviders, &out.InfrastructureProviders + *out = make([]NameConstraint, len(*in)) + copy(*out, *in) + } + if in.BootstrapProviders != nil { + in, out := &in.BootstrapProviders, &out.BootstrapProviders + *out = make([]NameConstraint, len(*in)) + copy(*out, *in) + } + if in.ControlPlaneProviders != nil { + in, out := &in.ControlPlaneProviders, &out.ControlPlaneProviders + *out = make([]NameConstraint, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProvidersConstrainted. +func (in *ProvidersConstrainted) DeepCopy() *ProvidersConstrainted { + if in == nil { + return nil + } + out := new(ProvidersConstrainted) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProvidersVersioned) DeepCopyInto(out *ProvidersVersioned) { + *out = *in + if in.InfrastructureProviders != nil { + in, out := &in.InfrastructureProviders, &out.InfrastructureProviders + *out = make([]NameVersion, len(*in)) + copy(*out, *in) + } + if in.BootstrapProviders != nil { + in, out := &in.BootstrapProviders, &out.BootstrapProviders + *out = make([]NameVersion, len(*in)) + copy(*out, *in) + } + if in.ControlPlaneProviders != nil { + in, out := &in.ControlPlaneProviders, &out.ControlPlaneProviders + *out = make([]NameVersion, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProvidersVersioned. +func (in *ProvidersVersioned) DeepCopy() *ProvidersVersioned { + if in == nil { + return nil + } + out := new(ProvidersVersioned) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Release) DeepCopyInto(out *Release) { *out = *in @@ -989,7 +1083,8 @@ func (in *ServiceTemplateList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceTemplateSpec) DeepCopyInto(out *ServiceTemplateSpec) { *out = *in - in.TemplateSpecCommon.DeepCopyInto(&out.TemplateSpecCommon) + in.Helm.DeepCopyInto(&out.Helm) + in.Providers.DeepCopyInto(&out.Providers) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceTemplateSpec. @@ -1005,6 +1100,7 @@ func (in *ServiceTemplateSpec) DeepCopy() *ServiceTemplateSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ServiceTemplateStatus) DeepCopyInto(out *ServiceTemplateStatus) { *out = *in + in.Providers.DeepCopyInto(&out.Providers) in.TemplateStatusCommon.DeepCopyInto(&out.TemplateStatusCommon) } @@ -1188,27 +1284,9 @@ func (in *TemplateManagementStatus) DeepCopy() *TemplateManagementStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TemplateSpecCommon) DeepCopyInto(out *TemplateSpecCommon) { - *out = *in - in.Helm.DeepCopyInto(&out.Helm) - in.Providers.DeepCopyInto(&out.Providers) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateSpecCommon. -func (in *TemplateSpecCommon) DeepCopy() *TemplateSpecCommon { - if in == nil { - return nil - } - out := new(TemplateSpecCommon) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *TemplateStatusCommon) DeepCopyInto(out *TemplateStatusCommon) { *out = *in - out.TemplateValidationStatus = in.TemplateValidationStatus if in.Config != nil { in, out := &in.Config, &out.Config *out = new(apiextensionsv1.JSON) @@ -1219,7 +1297,7 @@ func (in *TemplateStatusCommon) DeepCopyInto(out *TemplateStatusCommon) { *out = new(v2.CrossNamespaceSourceReference) **out = **in } - in.Providers.DeepCopyInto(&out.Providers) + out.TemplateValidationStatus = in.TemplateValidationStatus } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TemplateStatusCommon. diff --git a/cmd/main.go b/cmd/main.go index 0d558762..db60bff8 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -181,7 +181,6 @@ func main() { templateReconciler := controller.TemplateReconciler{ Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), SystemNamespace: currentNamespace, DefaultRegistryURL: defaultRegistryURL, DefaultRepoType: determinedRepositoryType, diff --git a/go.mod b/go.mod index 11f2ff7d..37210832 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/Mirantis/hmc go 1.22.0 require ( + github.com/Masterminds/semver/v3 v3.3.0 github.com/a8m/envsubst v1.4.2 github.com/cert-manager/cert-manager v1.15.3 github.com/fluxcd/helm-controller/api v1.0.1 @@ -34,7 +35,6 @@ require ( github.com/BurntSushi/toml v1.3.2 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/Microsoft/hcsshim v0.11.4 // indirect diff --git a/internal/controller/managedcluster_controller.go b/internal/controller/managedcluster_controller.go index 85ad6365..565867aa 100644 --- a/internal/controller/managedcluster_controller.go +++ b/internal/controller/managedcluster_controller.go @@ -478,7 +478,7 @@ func (r *ManagedClusterReconciler) releaseCluster(ctx context.Context, namespace // Associate the provider with it's GVK for _, provider := range providers { - gvk, ok := providerGVKs[provider] + gvk, ok := providerGVKs[provider.Name] if !ok { continue } @@ -501,13 +501,14 @@ func (r *ManagedClusterReconciler) releaseCluster(ctx context.Context, namespace return nil } -func (r *ManagedClusterReconciler) getProviders(ctx context.Context, templateNamespace, templateName string) ([]string, error) { +func (r *ManagedClusterReconciler) getProviders(ctx context.Context, templateNamespace, templateName string) ([]hmc.NameConstraint, error) { template := &hmc.ClusterTemplate{} templateRef := types.NamespacedName{Name: templateName, Namespace: templateNamespace} if err := r.Get(ctx, templateRef, template); err != nil { log.FromContext(ctx).Error(err, "Failed to get ClusterTemplate", "namespace", templateNamespace, "name", templateName) return nil, err } + return template.Status.Providers.InfrastructureProviders, nil } diff --git a/internal/controller/managedcluster_controller_test.go b/internal/controller/managedcluster_controller_test.go index 2ad111b0..59762432 100644 --- a/internal/controller/managedcluster_controller_test.go +++ b/internal/controller/managedcluster_controller_test.go @@ -73,13 +73,11 @@ var _ = Describe("ManagedCluster Controller", func() { Namespace: managedClusterNamespace, }, Spec: hmc.ClusterTemplateSpec{ - TemplateSpecCommon: hmc.TemplateSpecCommon{ - Helm: hmc.HelmSpec{ - ChartRef: &hcv2.CrossNamespaceSourceReference{ - Kind: "HelmChart", - Name: "ref-test", - Namespace: "default", - }, + Helm: hmc.HelmSpec{ + ChartRef: &hcv2.CrossNamespaceSourceReference{ + Kind: "HelmChart", + Name: "ref-test", + Namespace: "default", }, }, }, @@ -156,8 +154,6 @@ var _ = Describe("ManagedCluster Controller", func() { NamespacedName: typeNamespacedName, }) Expect(err).NotTo(HaveOccurred()) - // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. - // Example: If you expect a certain status condition after reconciliation, verify it here. }) }) }) diff --git a/internal/controller/management_controller.go b/internal/controller/management_controller.go index 2a264fcd..f48498db 100644 --- a/internal/controller/management_controller.go +++ b/internal/controller/management_controller.go @@ -88,7 +88,7 @@ func (r *ManagementReconciler) Update(ctx context.Context, management *hmc.Manag } var errs error - detectedProviders := hmc.Providers{} + detectedProviders := hmc.ProvidersVersioned{} detectedComponents := make(map[string]hmc.ComponentStatus) err := r.enableAdditionalComponents(ctx, management) @@ -234,6 +234,7 @@ func wrappedComponents(mgmt *hmc.Management, release *hmc.Release) []component { hmcComp.Template = release.Spec.HMC.Template } components = append(components, hmcComp) + capiComp := component{ Component: mgmt.Spec.Core.CAPI, helmReleaseName: hmc.CoreCAPIName, dependsOn: []meta.NamespacedObjectReference{{Name: hmc.CoreHMCName}}, @@ -257,6 +258,7 @@ func wrappedComponents(mgmt *hmc.Management, release *hmc.Release) []component { c.targetNamespace = hmc.ProviderSveltosTargetNamespace c.createNamespace = hmc.ProviderSveltosCreateNamespace } + components = append(components, c) } @@ -331,7 +333,7 @@ func (r *ManagementReconciler) enableAdditionalComponents(ctx context.Context, m func updateComponentsStatus( components map[string]hmc.ComponentStatus, - providers *hmc.Providers, + providers *hmc.ProvidersVersioned, componentName string, templateStatus hmc.ProviderTemplateStatus, err string, @@ -341,11 +343,13 @@ func updateComponentsStatus( Success: err == "", } - if err == "" { - providers.InfrastructureProviders = append(providers.InfrastructureProviders, templateStatus.Providers.InfrastructureProviders...) - providers.BootstrapProviders = append(providers.BootstrapProviders, templateStatus.Providers.BootstrapProviders...) - providers.ControlPlaneProviders = append(providers.ControlPlaneProviders, templateStatus.Providers.ControlPlaneProviders...) + if err != "" { + return } + + providers.InfrastructureProviders = append(providers.InfrastructureProviders, templateStatus.Providers.InfrastructureProviders...) + providers.BootstrapProviders = append(providers.BootstrapProviders, templateStatus.Providers.BootstrapProviders...) + providers.ControlPlaneProviders = append(providers.ControlPlaneProviders, templateStatus.Providers.ControlPlaneProviders...) } // SetupWithManager sets up the controller with the Manager. diff --git a/internal/controller/management_controller_test.go b/internal/controller/management_controller_test.go index 57dadd68..23c8c405 100644 --- a/internal/controller/management_controller_test.go +++ b/internal/controller/management_controller_test.go @@ -36,7 +36,7 @@ var _ = Describe("Management Controller", func() { typeNamespacedName := types.NamespacedName{ Name: resourceName, - Namespace: "default", // TODO(user):Modify as needed + Namespace: "default", } management := &hmcmirantiscomv1alpha1.Management{} @@ -49,14 +49,12 @@ var _ = Describe("Management Controller", func() { Name: resourceName, Namespace: "default", }, - // TODO(user): Specify other spec details if needed. } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) } }) AfterEach(func() { - // TODO(user): Cleanup logic after each test, like removing the resource instance. resource := &hmcmirantiscomv1alpha1.Management{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) @@ -75,8 +73,6 @@ var _ = Describe("Management Controller", func() { NamespacedName: typeNamespacedName, }) Expect(err).NotTo(HaveOccurred()) - // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. - // Example: If you expect a certain status condition after reconciliation, verify it here. }) }) }) diff --git a/internal/controller/release_controller.go b/internal/controller/release_controller.go index 43305f30..571b1933 100644 --- a/internal/controller/release_controller.go +++ b/internal/controller/release_controller.go @@ -126,9 +126,7 @@ func (p *Poller) getOrCreateManagement(ctx context.Context) (*hmc.Management, er return nil, err } - if err := mgmtObj.Spec.SetProvidersDefaults(); err != nil { - return nil, err - } + mgmtObj.Spec.Providers = hmc.GetDefaultProviders() getter := helm.NewMemoryRESTClientGetter(p.Config, p.RESTMapper()) actionConfig := new(action.Configuration) diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go index b2171e88..b6f21161 100644 --- a/internal/controller/template_controller.go +++ b/internal/controller/template_controller.go @@ -18,7 +18,6 @@ import ( "context" "encoding/json" "fmt" - "strings" helmcontrollerv2 "github.com/fluxcd/helm-controller/api/v2" "github.com/fluxcd/pkg/apis/meta" @@ -27,7 +26,6 @@ import ( apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -42,10 +40,9 @@ const ( defaultRepoName = "hmc-templates" ) -// TemplateReconciler reconciles a Template object +// TemplateReconciler reconciles a *Template object type TemplateReconciler struct { client.Client - Scheme *runtime.Scheme SystemNamespace string // DefaultRepoType is the type specified by default in HelmRepository @@ -73,29 +70,29 @@ type ProviderTemplateReconciler struct { } func (r *ClusterTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - l := log.FromContext(ctx).WithValues("ClusterTemplateController", req.NamespacedName) + l := log.FromContext(ctx) l.Info("Reconciling ClusterTemplate") - clusterTemplate := &hmc.ClusterTemplate{} - err := r.Get(ctx, req.NamespacedName, clusterTemplate) - if err != nil { + clusterTemplate := new(hmc.ClusterTemplate) + if err := r.Get(ctx, req.NamespacedName, clusterTemplate); err != nil { if apierrors.IsNotFound(err) { l.Info("ClusterTemplate not found, ignoring since object must be deleted") return ctrl.Result{}, nil } + l.Error(err, "Failed to get ClusterTemplate") return ctrl.Result{}, err } + return r.ReconcileTemplate(ctx, clusterTemplate) } func (r *ServiceTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - l := log.FromContext(ctx).WithValues("ServiceTemplateReconciler", req.NamespacedName) + l := log.FromContext(ctx) l.Info("Reconciling ServiceTemplate") - serviceTemplate := &hmc.ServiceTemplate{} - err := r.Get(ctx, req.NamespacedName, serviceTemplate) - if err != nil { + serviceTemplate := new(hmc.ServiceTemplate) + if err := r.Get(ctx, req.NamespacedName, serviceTemplate); err != nil { if apierrors.IsNotFound(err) { l.Info("ServiceTemplate not found, ignoring since object must be deleted") return ctrl.Result{}, nil @@ -107,44 +104,45 @@ func (r *ServiceTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Requ } func (r *ProviderTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - l := log.FromContext(ctx).WithValues("ProviderTemplateReconciler", req.NamespacedName) + l := log.FromContext(ctx) l.Info("Reconciling ProviderTemplate") - providerTemplate := &hmc.ProviderTemplate{} - err := r.Get(ctx, req.NamespacedName, providerTemplate) - if err != nil { + providerTemplate := new(hmc.ProviderTemplate) + if err := r.Get(ctx, req.NamespacedName, providerTemplate); err != nil { if apierrors.IsNotFound(err) { l.Info("ProviderTemplate not found, ignoring since object must be deleted") return ctrl.Result{}, nil } + l.Error(err, "Failed to get ProviderTemplate") return ctrl.Result{}, err } + return r.ReconcileTemplate(ctx, providerTemplate) } -// Template is the interface defining a list of methods to interact with templates -type Template interface { +type templateCommon interface { client.Object - GetSpec() *hmc.TemplateSpecCommon - GetStatus() *hmc.TemplateStatusCommon + GetHelmSpec() *hmc.HelmSpec + GetCommonStatus() *hmc.TemplateStatusCommon + FillStatusWithProviders(map[string]string) error } -func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template Template) (ctrl.Result, error) { +func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template templateCommon) (ctrl.Result, error) { l := log.FromContext(ctx) - spec := template.GetSpec() - status := template.GetStatus() + helmSpec := template.GetHelmSpec() + status := template.GetCommonStatus() var err error var hcChart *sourcev1.HelmChart - if spec.Helm.ChartRef != nil { - hcChart, err = r.getHelmChartFromChartRef(ctx, spec.Helm.ChartRef) + if helmSpec.ChartRef != nil { + hcChart, err = r.getHelmChartFromChartRef(ctx, helmSpec.ChartRef) if err != nil { - l.Error(err, "failed to get artifact from chartRef", "kind", spec.Helm.ChartRef.Kind, "namespace", spec.Helm.ChartRef.Namespace, "name", spec.Helm.ChartRef.Name) + l.Error(err, "failed to get artifact from chartRef", "chartRef", helmSpec.String()) return ctrl.Result{}, err } } else { - if spec.Helm.ChartName == "" { + if helmSpec.ChartName == "" { err := fmt.Errorf("neither chartName nor chartRef is set") l.Error(err, "invalid helm chart reference") return ctrl.Result{}, err @@ -196,19 +194,23 @@ func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template Tem _ = r.updateStatus(ctx, template, err.Error()) return ctrl.Result{}, err } + l.Info("Validating Helm chart") - if err := parseChartMetadata(template, helmChart); err != nil { - l.Error(err, "Failed to parse Helm chart metadata") + if err = helmChart.Validate(); err != nil { + l.Error(err, "Helm chart validation failed") _ = r.updateStatus(ctx, template, err.Error()) return ctrl.Result{}, err } - if err = helmChart.Validate(); err != nil { - l.Error(err, "Helm chart validation failed") + + l.Info("Parsing Helm chart metadata") + if err := fillStatusWithProviders(template, helmChart); err != nil { + l.Error(err, "Failed to fill status with providers") _ = r.updateStatus(ctx, template, err.Error()) return ctrl.Result{}, err } status.Description = helmChart.Metadata.Description + rawValues, err := json.Marshal(helmChart.Values) if err != nil { l.Error(err, "Failed to parse Helm chart values") @@ -217,52 +219,26 @@ func (r *TemplateReconciler) ReconcileTemplate(ctx context.Context, template Tem return ctrl.Result{}, err } status.Config = &apiextensionsv1.JSON{Raw: rawValues} + l.Info("Chart validation completed successfully") return ctrl.Result{}, r.updateStatus(ctx, template, "") } -func templateManagedByHMC(template Template) bool { +func templateManagedByHMC(template templateCommon) bool { return template.GetLabels()[hmc.HMCManagedLabelKey] == hmc.HMCManagedLabelValue } -func parseChartMetadata(template Template, inChart *chart.Chart) error { - if inChart.Metadata == nil { +func fillStatusWithProviders(template templateCommon, helmChart *chart.Chart) error { + if helmChart.Metadata == nil { return fmt.Errorf("chart metadata is empty") } - spec := template.GetSpec() - status := template.GetStatus() - // the value in spec has higher priority - if len(spec.Providers.InfrastructureProviders) > 0 { - status.Providers.InfrastructureProviders = spec.Providers.InfrastructureProviders - } else { - infraProviders := inChart.Metadata.Annotations[hmc.ChartAnnotationInfraProviders] - if infraProviders != "" { - status.Providers.InfrastructureProviders = strings.Split(infraProviders, ",") - } - } - if len(spec.Providers.BootstrapProviders) > 0 { - status.Providers.BootstrapProviders = spec.Providers.BootstrapProviders - } else { - bootstrapProviders := inChart.Metadata.Annotations[hmc.ChartAnnotationBootstrapProviders] - if bootstrapProviders != "" { - status.Providers.BootstrapProviders = strings.Split(bootstrapProviders, ",") - } - } - if len(spec.Providers.ControlPlaneProviders) > 0 { - status.Providers.ControlPlaneProviders = spec.Providers.ControlPlaneProviders - } else { - cpProviders := inChart.Metadata.Annotations[hmc.ChartAnnotationControlPlaneProviders] - if cpProviders != "" { - status.Providers.ControlPlaneProviders = strings.Split(cpProviders, ",") - } - } - return nil + return template.FillStatusWithProviders(helmChart.Metadata.Annotations) } -func (r *TemplateReconciler) updateStatus(ctx context.Context, template Template, validationError string) error { - status := template.GetStatus() +func (r *TemplateReconciler) updateStatus(ctx context.Context, template templateCommon, validationError string) error { + status := template.GetCommonStatus() status.ObservedGeneration = template.GetGeneration() status.ValidationError = validationError status.Valid = validationError == "" @@ -312,8 +288,7 @@ func (r *TemplateReconciler) reconcileDefaultHelmRepository(ctx context.Context, return nil } -func (r *TemplateReconciler) reconcileHelmChart(ctx context.Context, template Template) (*sourcev1.HelmChart, error) { - spec := template.GetSpec() +func (r *TemplateReconciler) reconcileHelmChart(ctx context.Context, template templateCommon) (*sourcev1.HelmChart, error) { namespace := template.GetNamespace() if namespace == "" { namespace = r.SystemNamespace @@ -325,10 +300,12 @@ func (r *TemplateReconciler) reconcileHelmChart(ctx context.Context, template Te }, } + helmSpec := template.GetHelmSpec() _, err := ctrl.CreateOrUpdate(ctx, r.Client, helmChart, func() error { if helmChart.Labels == nil { helmChart.Labels = make(map[string]string) } + helmChart.Labels[hmc.HMCManagedLabelKey] = hmc.HMCManagedLabelValue helmChart.OwnerReferences = []metav1.OwnerReference{ { @@ -338,21 +315,21 @@ func (r *TemplateReconciler) reconcileHelmChart(ctx context.Context, template Te UID: template.GetUID(), }, } + helmChart.Spec = sourcev1.HelmChartSpec{ - Chart: spec.Helm.ChartName, - Version: spec.Helm.ChartVersion, + Chart: helmSpec.ChartName, + Version: helmSpec.ChartVersion, SourceRef: sourcev1.LocalHelmChartSourceReference{ Kind: sourcev1.HelmRepositoryKind, Name: defaultRepoName, }, Interval: metav1.Duration{Duration: helm.DefaultReconcileInterval}, } + return nil }) - if err != nil { - return nil, err - } - return helmChart, nil + + return helmChart, err } func (r *TemplateReconciler) getHelmChartFromChartRef(ctx context.Context, chartRef *helmcontrollerv2.CrossNamespaceSourceReference) (*sourcev1.HelmChart, error) { diff --git a/internal/controller/template_controller_test.go b/internal/controller/template_controller_test.go index 8a95ed2e..e206ea99 100644 --- a/internal/controller/template_controller_test.go +++ b/internal/controller/template_controller_test.go @@ -52,7 +52,7 @@ var _ = Describe("Template Controller", func() { typeNamespacedName := types.NamespacedName{ Name: resourceName, - Namespace: "default", // TODO(user):Modify as needed + Namespace: "default", } clusterTemplate := &hmcmirantiscomv1alpha1.ClusterTemplate{} serviceTemplate := &hmcmirantiscomv1alpha1.ServiceTemplate{} @@ -60,13 +60,11 @@ var _ = Describe("Template Controller", func() { helmRepo := &sourcev1.HelmRepository{} helmChart := &sourcev1.HelmChart{} - templateSpec := hmcmirantiscomv1alpha1.TemplateSpecCommon{ - Helm: hmcmirantiscomv1alpha1.HelmSpec{ - ChartRef: &helmcontrollerv2.CrossNamespaceSourceReference{ - Kind: "HelmChart", - Name: helmChartName, - Namespace: helmRepoNamespace, - }, + helmSpec := hmcmirantiscomv1alpha1.HelmSpec{ + ChartRef: &helmcontrollerv2.CrossNamespaceSourceReference{ + Kind: "HelmChart", + Name: helmChartName, + Namespace: helmRepoNamespace, }, } @@ -120,7 +118,7 @@ var _ = Describe("Template Controller", func() { Name: resourceName, Namespace: "default", }, - Spec: hmcmirantiscomv1alpha1.ClusterTemplateSpec{TemplateSpecCommon: templateSpec}, + Spec: hmcmirantiscomv1alpha1.ClusterTemplateSpec{Helm: helmSpec}, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) } @@ -132,7 +130,7 @@ var _ = Describe("Template Controller", func() { Name: resourceName, Namespace: "default", }, - Spec: hmcmirantiscomv1alpha1.ServiceTemplateSpec{TemplateSpecCommon: templateSpec}, + Spec: hmcmirantiscomv1alpha1.ServiceTemplateSpec{Helm: helmSpec}, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) } @@ -144,14 +142,13 @@ var _ = Describe("Template Controller", func() { Name: resourceName, Namespace: "default", }, - Spec: hmcmirantiscomv1alpha1.ProviderTemplateSpec{TemplateSpecCommon: templateSpec}, + Spec: hmcmirantiscomv1alpha1.ProviderTemplateSpec{Helm: helmSpec}, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) } }) AfterEach(func() { - // TODO(user): Cleanup logic after each test, like removing the resource instance. clusterTemplateResource := &hmcmirantiscomv1alpha1.ClusterTemplate{} err := k8sClient.Get(ctx, typeNamespacedName, clusterTemplateResource) Expect(err).NotTo(HaveOccurred()) @@ -176,7 +173,6 @@ var _ = Describe("Template Controller", func() { It("should successfully reconcile the resource", func() { templateReconciler := TemplateReconciler{ Client: k8sClient, - Scheme: k8sClient.Scheme(), downloadHelmChartFunc: fakeDownloadHelmChartFunc, } By("Reconciling the ClusterTemplate resource") @@ -193,8 +189,6 @@ var _ = Describe("Template Controller", func() { providerTemplateReconciler := &ProviderTemplateReconciler{TemplateReconciler: templateReconciler} _, err = providerTemplateReconciler.Reconcile(ctx, reconcile.Request{NamespacedName: typeNamespacedName}) Expect(err).NotTo(HaveOccurred()) - // TODO(user): Add more specific assertions depending on your controller's reconciliation logic. - // Example: If you expect a certain status condition after reconciliation, verify it here. }) }) }) diff --git a/internal/controller/templatemanagement_controller.go b/internal/controller/templatemanagement_controller.go index 745b6464..11ff813d 100644 --- a/internal/controller/templatemanagement_controller.go +++ b/internal/controller/templatemanagement_controller.go @@ -153,13 +153,11 @@ func (r *TemplateManagementReconciler) applyTemplates(ctx context.Context, kind return fmt.Errorf("invalid kind %s. Only %s or %s kinds are supported", kind, templateutil.ClusterTemplateKind, templateutil.ServiceTemplateKind) } - spec := hmc.TemplateSpecCommon{ - Helm: hmc.HelmSpec{ - ChartRef: &helmcontrollerv2.CrossNamespaceSourceReference{ - Kind: sourcev1.HelmChartKind, - Name: chartName, - Namespace: r.SystemNamespace, - }, + helmSpec := hmc.HelmSpec{ + ChartRef: &helmcontrollerv2.CrossNamespaceSourceReference{ + Kind: sourcev1.HelmChartKind, + Name: chartName, + Namespace: r.SystemNamespace, }, } var errs error @@ -167,10 +165,10 @@ func (r *TemplateManagementReconciler) applyTemplates(ctx context.Context, kind var target client.Object meta.Namespace = ns if kind == templateutil.ClusterTemplateKind { - target = &hmc.ClusterTemplate{ObjectMeta: meta, Spec: hmc.ClusterTemplateSpec{TemplateSpecCommon: spec}} + target = &hmc.ClusterTemplate{ObjectMeta: meta, Spec: hmc.ClusterTemplateSpec{Helm: helmSpec}} } if kind == templateutil.ServiceTemplateKind { - target = &hmc.ServiceTemplate{ObjectMeta: meta, Spec: hmc.ServiceTemplateSpec{TemplateSpecCommon: spec}} + target = &hmc.ServiceTemplate{ObjectMeta: meta, Spec: hmc.ServiceTemplateSpec{Helm: helmSpec}} } if keep { if !sourceFound { diff --git a/internal/telemetry/tracker.go b/internal/telemetry/tracker.go index fbabb12b..3f5662b0 100644 --- a/internal/telemetry/tracker.go +++ b/internal/telemetry/tracker.go @@ -96,9 +96,9 @@ func (t *Tracker) trackManagedClusterHeartbeat(ctx context.Context) error { clusterID, managedCluster.Spec.Template, template.Spec.Helm.ChartVersion, - strings.Join(template.Status.Providers.InfrastructureProviders, ","), - strings.Join(template.Status.Providers.BootstrapProviders, ","), - strings.Join(template.Status.Providers.ControlPlaneProviders, ","), + strings.Join(template.Status.Providers.InfrastructureProvidersNames(), ","), + strings.Join(template.Status.Providers.BootstrapProvidersNames(), ","), + strings.Join(template.Status.Providers.ControlPlaneProvidersNames(), ","), ) if err != nil { errs = errors.Join(errs, fmt.Errorf("failed to track the heartbeat of the managedcluster %s/%s", managedCluster.Namespace, managedCluster.Name)) diff --git a/internal/utils/util.go b/internal/utils/util.go deleted file mode 100644 index 35c60ea3..00000000 --- a/internal/utils/util.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2024 -// -// 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 utils - -// SliceToMapKeys converts a given slice to a map with slice's values -// as the map's keys zeroing value for each. -func SliceToMapKeys[S ~[]K, M ~map[K]V, K comparable, V any](s S) M { - m := make(M) - for i := range s { - m[s[i]] = *new(V) - } - return m -} - -// DiffSliceSubset finds missing items of a given slice in a given map. -// If the slice is a subset of the map, returns empty slice. -// Boolean return argument indicates whether the slice is a subset. -func DiffSliceSubset[S ~[]K, M ~map[K]V, K comparable, V any](s S, m M) (diff S, isSubset bool) { - for _, v := range s { - if _, ok := m[v]; !ok { - diff = append(diff, v) - } - } - - return diff, len(diff) == 0 -} diff --git a/internal/webhook/managedcluster_webhook.go b/internal/webhook/managedcluster_webhook.go index 48b5e221..00535426 100644 --- a/internal/webhook/managedcluster_webhook.go +++ b/internal/webhook/managedcluster_webhook.go @@ -30,7 +30,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" "github.com/Mirantis/hmc/api/v1alpha1" - "github.com/Mirantis/hmc/internal/utils" ) type ManagedClusterValidator struct { @@ -171,11 +170,17 @@ func (v *ManagedClusterValidator) verifyProviders(ctx context.Context, template return nil } -func getMissingProviders(exposedProviders []string, requiredProviders []string) []string { - exposedBootstrapProviders := utils.SliceToMapKeys[[]string, map[string]struct{}](exposedProviders) - diff, isSubset := utils.DiffSliceSubset(requiredProviders, exposedBootstrapProviders) - if !isSubset { - return diff +func getMissingProviders(exposedProviders []v1alpha1.NameVersion, requiredProviders []v1alpha1.NameConstraint) (missing []string) { + exposedSet := make(map[string]struct{}, len(requiredProviders)) + for _, v := range exposedProviders { + exposedSet[v.Name] = struct{}{} } - return []string{} + + for _, v := range requiredProviders { + if _, ok := exposedSet[v.Name]; !ok { + missing = append(missing, v.Name) + } + } + + return missing } diff --git a/internal/webhook/managedcluster_webhook_test.go b/internal/webhook/managedcluster_webhook_test.go index 61e5a02d..59b31f3e 100644 --- a/internal/webhook/managedcluster_webhook_test.go +++ b/internal/webhook/managedcluster_webhook_test.go @@ -36,10 +36,10 @@ var ( testNamespace = "test" mgmt = management.NewManagement( - management.WithAvailableProviders(v1alpha1.Providers{ - InfrastructureProviders: []string{"aws"}, - BootstrapProviders: []string{"k0s"}, - ControlPlaneProviders: []string{"k0s"}, + management.WithAvailableProviders(v1alpha1.ProvidersVersioned{ + InfrastructureProviders: []v1alpha1.NameVersion{{Name: "aws"}}, + BootstrapProviders: []v1alpha1.NameVersion{{Name: "k0s"}}, + ControlPlaneProviders: []v1alpha1.NameVersion{{Name: "k0s"}}, }), ) @@ -87,17 +87,17 @@ var ( managedCluster: managedcluster.NewManagedCluster(managedcluster.WithTemplate(testTemplateName)), existingObjects: []runtime.Object{ management.NewManagement( - management.WithAvailableProviders(v1alpha1.Providers{ - InfrastructureProviders: []string{"aws"}, - BootstrapProviders: []string{"k0s"}, + management.WithAvailableProviders(v1alpha1.ProvidersVersioned{ + InfrastructureProviders: []v1alpha1.NameVersion{{Name: "aws"}}, + BootstrapProviders: []v1alpha1.NameVersion{{Name: "k0s"}}, }), ), template.NewClusterTemplate( template.WithName(testTemplateName), - template.WithProvidersStatus(v1alpha1.Providers{ - InfrastructureProviders: []string{"azure"}, - BootstrapProviders: []string{"k0s"}, - ControlPlaneProviders: []string{"k0s"}, + template.WithProvidersStatus(v1alpha1.ProvidersConstrainted{ + InfrastructureProviders: []v1alpha1.NameConstraint{{Name: "azure"}}, + BootstrapProviders: []v1alpha1.NameConstraint{{Name: "k0s"}}, + ControlPlaneProviders: []v1alpha1.NameConstraint{{Name: "k0s"}}, }), template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), ), @@ -111,10 +111,10 @@ var ( mgmt, template.NewClusterTemplate( template.WithName(testTemplateName), - template.WithProvidersStatus(v1alpha1.Providers{ - InfrastructureProviders: []string{"aws"}, - BootstrapProviders: []string{"k0s"}, - ControlPlaneProviders: []string{"k0s"}, + template.WithProvidersStatus(v1alpha1.ProvidersConstrainted{ + InfrastructureProviders: []v1alpha1.NameConstraint{{Name: "aws"}}, + BootstrapProviders: []v1alpha1.NameConstraint{{Name: "k0s"}}, + ControlPlaneProviders: []v1alpha1.NameConstraint{{Name: "k0s"}}, }), template.WithValidationStatus(v1alpha1.TemplateValidationStatus{Valid: true}), ), diff --git a/templates/provider/hmc/templates/crds/hmc.mirantis.com_clustertemplates.yaml b/templates/provider/hmc/templates/crds/hmc.mirantis.com_clustertemplates.yaml index ab9fe08d..02600d30 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_clustertemplates.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_clustertemplates.yaml @@ -57,8 +57,8 @@ spec: description: ClusterTemplateSpec defines the desired state of ClusterTemplate properties: helm: - description: Helm holds a reference to a Helm chart representing the - HMC template + description: HelmSpec references a Helm chart representing the HMC + template properties: chartName: description: ChartName is a name of a Helm chart representing @@ -103,28 +103,59 @@ spec: - message: either chartName or chartRef must be set rule: (has(self.chartName) && !has(self.chartRef)) || (!has(self.chartName) && has(self.chartRef)) + k8sVersion: + description: Compatible K8S version of the cluster set in the SemVer + format. + type: string providers: description: |- Providers represent required/exposed CAPI providers depending on the template type. Should be set if not present in the Helm chart metadata. properties: bootstrap: - description: BootstrapProviders is the list of CAPI bootstrap - providers + description: List of CAPI bootstrap providers with the SemVer + constraint. items: - type: string + description: NameConstraint represents name of the provider + with its constrainted version. + properties: + constraint: + description: Supported versions set via the SemVer constraint. + type: string + name: + description: Name of the provider. + type: string + type: object type: array controlPlane: - description: ControlPlaneProviders is the list of CAPI control - plane providers + description: List of CAPI control plane providers with the SemVer + constraint. items: - type: string + description: NameConstraint represents name of the provider + with its constrainted version. + properties: + constraint: + description: Supported versions set via the SemVer constraint. + type: string + name: + description: Name of the provider. + type: string + type: object type: array infrastructure: - description: InfrastructureProviders is the list of CAPI infrastructure - providers + description: List of CAPI infrastructure providers with the SemVer + constraint. items: - type: string + description: NameConstraint represents name of the provider + with its constrainted version. + properties: + constraint: + description: Supported versions set via the SemVer constraint. + type: string + name: + description: Name of the provider. + type: string + type: object type: array type: object required: @@ -183,22 +214,49 @@ spec: on the template type. properties: bootstrap: - description: BootstrapProviders is the list of CAPI bootstrap - providers + description: List of CAPI bootstrap providers with the SemVer + constraint. items: - type: string + description: NameConstraint represents name of the provider + with its constrainted version. + properties: + constraint: + description: Supported versions set via the SemVer constraint. + type: string + name: + description: Name of the provider. + type: string + type: object type: array controlPlane: - description: ControlPlaneProviders is the list of CAPI control - plane providers + description: List of CAPI control plane providers with the SemVer + constraint. items: - type: string + description: NameConstraint represents name of the provider + with its constrainted version. + properties: + constraint: + description: Supported versions set via the SemVer constraint. + type: string + name: + description: Name of the provider. + type: string + type: object type: array infrastructure: - description: InfrastructureProviders is the list of CAPI infrastructure - providers + description: List of CAPI infrastructure providers with the SemVer + constraint. items: - type: string + description: NameConstraint represents name of the provider + with its constrainted version. + properties: + constraint: + description: Supported versions set via the SemVer constraint. + type: string + name: + description: Name of the provider. + type: string + type: object type: array type: object valid: diff --git a/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml b/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml index dc9b67c6..c12e423e 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml @@ -12,8 +12,8 @@ spec: listKind: ManagedClusterList plural: managedclusters shortNames: - - hmc-deploy - - deploy + - hmcmc + - mc singular: managedcluster scope: Namespaced versions: @@ -137,6 +137,11 @@ spec: - type type: object type: array + k8sVersion: + description: |- + Currently compatible K8S version of the cluster. Being set only if + the corresponding ClusterTemplate provided it in the spec. + type: string observedGeneration: description: ObservedGeneration is the last observed generation. format: int64 diff --git a/templates/provider/hmc/templates/crds/hmc.mirantis.com_managements.yaml b/templates/provider/hmc/templates/crds/hmc.mirantis.com_managements.yaml index 21aed29a..e0e4e1bd 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_managements.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_managements.yaml @@ -112,26 +112,54 @@ spec: description: ManagementStatus defines the observed state of Management properties: availableProviders: - description: AvailableProviders holds all CAPI providers available - on the Management cluster. + description: |- + AvailableProviders holds all CAPI providers available along with + their versions if specified in ProviderTemplates on the Management cluster. properties: bootstrap: - description: BootstrapProviders is the list of CAPI bootstrap - providers + description: List of CAPI bootstrap providers with the supported + version. items: - type: string + description: NameVersion represents name of the provider with + its supported version. + properties: + name: + description: Name of the provider. + type: string + version: + description: Supported exact version in the SemVer format. + type: string + type: object type: array controlPlane: - description: ControlPlaneProviders is the list of CAPI control - plane providers + description: List of CAPI control plane providers with the supported + version. items: - type: string + description: NameVersion represents name of the provider with + its supported version. + properties: + name: + description: Name of the provider. + type: string + version: + description: Supported exact version in the SemVer format. + type: string + type: object type: array infrastructure: - description: InfrastructureProviders is the list of CAPI infrastructure - providers + description: List of CAPI infrastructure providers with the supported + version. items: - type: string + description: NameVersion represents name of the provider with + its supported version. + properties: + name: + description: Name of the provider. + type: string + version: + description: Supported exact version in the SemVer format. + type: string + type: object type: array type: object components: diff --git a/templates/provider/hmc/templates/crds/hmc.mirantis.com_providertemplates.yaml b/templates/provider/hmc/templates/crds/hmc.mirantis.com_providertemplates.yaml index 5249d0bd..fa1d94ce 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_providertemplates.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_providertemplates.yaml @@ -56,9 +56,12 @@ spec: spec: description: ProviderTemplateSpec defines the desired state of ProviderTemplate properties: + capiVersion: + description: Compatible CAPI provider version set in the SemVer format. + type: string helm: - description: Helm holds a reference to a Helm chart representing the - HMC template + description: HelmSpec references a Helm chart representing the HMC + template properties: chartName: description: ChartName is a name of a Helm chart representing @@ -109,22 +112,49 @@ spec: Should be set if not present in the Helm chart metadata. properties: bootstrap: - description: BootstrapProviders is the list of CAPI bootstrap - providers + description: List of CAPI bootstrap providers with the supported + version. items: - type: string + description: NameVersion represents name of the provider with + its supported version. + properties: + name: + description: Name of the provider. + type: string + version: + description: Supported exact version in the SemVer format. + type: string + type: object type: array controlPlane: - description: ControlPlaneProviders is the list of CAPI control - plane providers + description: List of CAPI control plane providers with the supported + version. items: - type: string + description: NameVersion represents name of the provider with + its supported version. + properties: + name: + description: Name of the provider. + type: string + version: + description: Supported exact version in the SemVer format. + type: string + type: object type: array infrastructure: - description: InfrastructureProviders is the list of CAPI infrastructure - providers + description: List of CAPI infrastructure providers with the supported + version. items: - type: string + description: NameVersion represents name of the provider with + its supported version. + properties: + name: + description: Name of the provider. + type: string + version: + description: Supported exact version in the SemVer format. + type: string + type: object type: array type: object required: @@ -183,22 +213,49 @@ spec: on the template type. properties: bootstrap: - description: BootstrapProviders is the list of CAPI bootstrap - providers + description: List of CAPI bootstrap providers with the supported + version. items: - type: string + description: NameVersion represents name of the provider with + its supported version. + properties: + name: + description: Name of the provider. + type: string + version: + description: Supported exact version in the SemVer format. + type: string + type: object type: array controlPlane: - description: ControlPlaneProviders is the list of CAPI control - plane providers + description: List of CAPI control plane providers with the supported + version. items: - type: string + description: NameVersion represents name of the provider with + its supported version. + properties: + name: + description: Name of the provider. + type: string + version: + description: Supported exact version in the SemVer format. + type: string + type: object type: array infrastructure: - description: InfrastructureProviders is the list of CAPI infrastructure - providers + description: List of CAPI infrastructure providers with the supported + version. items: - type: string + description: NameVersion represents name of the provider with + its supported version. + properties: + name: + description: Name of the provider. + type: string + version: + description: Supported exact version in the SemVer format. + type: string + type: object type: array type: object valid: diff --git a/templates/provider/hmc/templates/crds/hmc.mirantis.com_servicetemplates.yaml b/templates/provider/hmc/templates/crds/hmc.mirantis.com_servicetemplates.yaml index e3747b9d..04bfe844 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_servicetemplates.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_servicetemplates.yaml @@ -57,8 +57,8 @@ spec: description: ServiceTemplateSpec defines the desired state of ServiceTemplate properties: helm: - description: Helm holds a reference to a Helm chart representing the - HMC template + description: HelmSpec references a Helm chart representing the HMC + template properties: chartName: description: ChartName is a name of a Helm chart representing @@ -103,6 +103,10 @@ spec: - message: either chartName or chartRef must be set rule: (has(self.chartName) && !has(self.chartRef)) || (!has(self.chartName) && has(self.chartRef)) + k8sConstraint: + description: Constraint describing compatible K8S versions of the + cluster set in the SemVer format. + type: string providers: description: |- Providers represent required/exposed CAPI providers depending on the template type. diff --git a/test/objects/management/management.go b/test/objects/management/management.go index 26a31899..09fd8de8 100644 --- a/test/objects/management/management.go +++ b/test/objects/management/management.go @@ -64,7 +64,7 @@ func WithProviders(providers []v1alpha1.Provider) Opt { } } -func WithAvailableProviders(providers v1alpha1.Providers) Opt { +func WithAvailableProviders(providers v1alpha1.ProvidersVersioned) Opt { return func(p *v1alpha1.Management) { p.Status.AvailableProviders = providers } diff --git a/test/objects/template/template.go b/test/objects/template/template.go index 668f4019..88235d4a 100644 --- a/test/objects/template/template.go +++ b/test/objects/template/template.go @@ -15,8 +15,11 @@ package template import ( + "fmt" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/Mirantis/hmc/api/v1alpha1" ) @@ -26,99 +29,124 @@ const ( DefaultNamespace = "default" ) -type Template struct { - metav1.ObjectMeta `json:",inline"` - Spec v1alpha1.TemplateSpecCommon `json:"spec"` - Status v1alpha1.TemplateStatusCommon `json:"status"` -} +type ( + Opt func(template Template) -type Opt func(template *Template) + Template interface { + client.Object + GetHelmSpec() *v1alpha1.HelmSpec + GetCommonStatus() *v1alpha1.TemplateStatusCommon + } +) func NewClusterTemplate(opts ...Opt) *v1alpha1.ClusterTemplate { - templateState := NewTemplate(opts...) - return &v1alpha1.ClusterTemplate{ - ObjectMeta: templateState.ObjectMeta, - Spec: v1alpha1.ClusterTemplateSpec{TemplateSpecCommon: templateState.Spec}, - Status: v1alpha1.ClusterTemplateStatus{TemplateStatusCommon: templateState.Status}, + t := &v1alpha1.ClusterTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: DefaultName, + Namespace: DefaultNamespace, + }, + } + + for _, o := range opts { + o(t) } + + return t } func NewServiceTemplate(opts ...Opt) *v1alpha1.ServiceTemplate { - templateState := NewTemplate(opts...) - return &v1alpha1.ServiceTemplate{ - ObjectMeta: templateState.ObjectMeta, - Spec: v1alpha1.ServiceTemplateSpec{TemplateSpecCommon: templateState.Spec}, - Status: v1alpha1.ServiceTemplateStatus{TemplateStatusCommon: templateState.Status}, + t := &v1alpha1.ServiceTemplate{ + ObjectMeta: metav1.ObjectMeta{ + Name: DefaultName, + Namespace: DefaultNamespace, + }, } -} -func NewProviderTemplate(opts ...Opt) *v1alpha1.ProviderTemplate { - templateState := NewTemplate(opts...) - return &v1alpha1.ProviderTemplate{ - ObjectMeta: templateState.ObjectMeta, - Spec: v1alpha1.ProviderTemplateSpec{TemplateSpecCommon: templateState.Spec}, - Status: v1alpha1.ProviderTemplateStatus{TemplateStatusCommon: templateState.Status}, + for _, o := range opts { + o(t) } + + return t } -func NewTemplate(opts ...Opt) *Template { - template := &Template{ +func NewProviderTemplate(opts ...Opt) *v1alpha1.ProviderTemplate { + t := &v1alpha1.ProviderTemplate{ ObjectMeta: metav1.ObjectMeta{ Name: DefaultName, Namespace: DefaultNamespace, }, } - for _, opt := range opts { - opt(template) + + for _, o := range opts { + o(t) } - return template + + return t } func WithName(name string) Opt { - return func(t *Template) { - t.Name = name + return func(t Template) { + t.SetName(name) } } func WithNamespace(namespace string) Opt { - return func(t *Template) { - t.Namespace = namespace + return func(t Template) { + t.SetNamespace(namespace) } } func WithLabels(labels map[string]string) Opt { - return func(t *Template) { - t.Labels = labels + return func(t Template) { + t.SetLabels(labels) } } func WithHelmSpec(helmSpec v1alpha1.HelmSpec) Opt { - return func(t *Template) { - t.Spec.Helm = helmSpec - } -} - -func WithProviders(providers v1alpha1.Providers) Opt { - return func(t *Template) { - t.Spec.Providers = providers + return func(t Template) { + spec := t.GetHelmSpec() + spec.ChartName = helmSpec.ChartName + spec.ChartRef = helmSpec.ChartRef + spec.ChartVersion = helmSpec.ChartVersion } } func WithValidationStatus(validationStatus v1alpha1.TemplateValidationStatus) Opt { - return func(t *Template) { - t.Status.TemplateValidationStatus = validationStatus + return func(t Template) { + status := t.GetCommonStatus() + status.TemplateValidationStatus = validationStatus } } -func WithProvidersStatus(providers v1alpha1.Providers) Opt { - return func(t *Template) { - t.Status.Providers = providers +func WithProvidersStatus[T v1alpha1.Providers | v1alpha1.ProvidersVersioned | v1alpha1.ProvidersConstrainted](providers T) Opt { + return func(t Template) { + switch v := t.(type) { + case *v1alpha1.ClusterTemplate: + var ok bool + v.Status.Providers, ok = any(providers).(v1alpha1.ProvidersConstrainted) + if !ok { + panic(fmt.Sprintf("unexpected type %T", providers)) + } + case *v1alpha1.ProviderTemplate: + var ok bool + v.Status.Providers, ok = any(providers).(v1alpha1.ProvidersVersioned) + if !ok { + panic(fmt.Sprintf("unexpected type %T", providers)) + } + case *v1alpha1.ServiceTemplate: + var ok bool + v.Status.Providers, ok = any(providers).(v1alpha1.Providers) + if !ok { + panic(fmt.Sprintf("unexpected type %T", providers)) + } + } } } func WithConfigStatus(config string) Opt { - return func(t *Template) { - t.Status.Config = &apiextensionsv1.JSON{ + return func(t Template) { + status := t.GetCommonStatus() + status.Config = &apiextensionsv1.JSON{ Raw: []byte(config), } }