diff --git a/.golangci.yml b/.golangci.yml index a2a5e7ed..f47fe05b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -68,7 +68,7 @@ linters-settings: - pattern: "interface{}" replacement: "any" stylecheck: - checks: ["all", "-ST1000", "-ST1001"] + checks: ["all", "-ST1000", "-ST1001", "-ST1021"] revive: enable-all-rules: true rules: diff --git a/api/v1alpha1/clustertemplate_types.go b/api/v1alpha1/clustertemplate_types.go index 36db285e..0d4127ff 100644 --- a/api/v1alpha1/clustertemplate_types.go +++ b/api/v1alpha1/clustertemplate_types.go @@ -15,6 +15,9 @@ package v1alpha1 import ( + "fmt" + + "github.com/Masterminds/semver/v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -22,19 +25,62 @@ const ClusterTemplateKind = "ClusterTemplate" // 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 CAPI providers with constrainted compatibility versions set. Should be set if not present in the Helm chart metadata. + Providers ProvidersTupled `json:"providers,omitempty"` } // ClusterTemplateStatus defines the observed state of ClusterTemplate type ClusterTemplateStatus struct { + // Compatible K8S version of the cluster set in the SemVer format. + KubertenesVersion string `json:"k8sVersion,omitempty"` + // Providers represent exposed CAPI providers with constrainted compatibility versions set. + Providers ProvidersTupled `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 { + var err error + t.Status.Providers.BootstrapProviders, err = parseProviders(t, bootstrapProvidersType, annotations, semver.NewConstraint) + if err != nil { + return fmt.Errorf("failed to parse ClusterTemplate bootstrap providers: %v", err) + } + + t.Status.Providers.ControlPlaneProviders, err = parseProviders(t, controlPlaneProvidersType, annotations, semver.NewConstraint) + if err != nil { + return fmt.Errorf("failed to parse ClusterTemplate controlPlane providers: %v", err) + } + + t.Status.Providers.InfrastructureProviders, err = parseProviders(t, infrastructureProvidersType, annotations, semver.NewConstraint) + if err != nil { + return fmt.Errorf("failed to parse ClusterTemplate infrastructure providers: %v", err) + } + + return nil +} + +// GetSpecProviders returns .spec.providers of the Template. +func (t *ClusterTemplate) GetSpecProviders() ProvidersTupled { + return t.Spec.Providers +} + +// GetStatusProviders returns .status.providers of the Template. +func (t *ClusterTemplate) GetStatusProviders() ProvidersTupled { + return t.Status.Providers +} + +// GetHelmSpec returns .spec.helm of the Template. +func (t *ClusterTemplate) GetHelmSpec() *HelmSpec { + return &t.Spec.Helm } -func (t *ClusterTemplate) GetStatus() *TemplateStatusCommon { +// GetCommonStatus returns common status of the Template. +func (t *ClusterTemplate) GetCommonStatus() *TemplateStatusCommon { return &t.Status.TemplateStatusCommon } diff --git a/api/v1alpha1/common.go b/api/v1alpha1/common.go index 7ec364f6..936b442b 100644 --- a/api/v1alpha1/common.go +++ b/api/v1alpha1/common.go @@ -21,15 +21,37 @@ 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"` + } + + // Holds different types of CAPI providers with either + // an exact or constrainted version in the SemVer format. The requirement + // is determined by a consumer this type. + ProvidersTupled struct { + // List of CAPI infrastructure providers with either an exact or constrainted version in the SemVer format. + InfrastructureProviders []ProviderTuple `json:"infrastructure,omitempty"` + // List of CAPI bootstrap providers with either an exact or constrainted version in the SemVer format. + BootstrapProviders []ProviderTuple `json:"bootstrap,omitempty"` + // List of CAPI control plane providers with either an exact or constrainted version in the SemVer format. + ControlPlaneProviders []ProviderTuple `json:"controlPlane,omitempty"` + } + + // Represents name of the provider with either an exact or constrainted version in the SemVer format. + ProviderTuple struct { + // Name of the provider. + Name string `json:"name,omitempty"` + // Compatibility restriction in the SemVer format (exact or constrainted version) + VersionOrContraint string `json:"versionOrContraint,omitempty"` + } +) const ( // Provider CAPA @@ -104,3 +126,36 @@ func ExtractServiceTemplateName(rawObj client.Object) []string { return templates } + +func (c ProvidersTupled) BootstrapProvidersNames() []string { + return c.names(bootstrapProvidersType) +} + +func (c ProvidersTupled) ControlPlaneProvidersNames() []string { + return c.names(bootstrapProvidersType) +} + +func (c ProvidersTupled) InfrastructureProvidersNames() []string { + return c.names(bootstrapProvidersType) +} + +func (c ProvidersTupled) names(typ providersType) []string { + f := func(nn []ProviderTuple) []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 c57ea749..fa7a8331 100644 --- a/api/v1alpha1/managedcluster_types.go +++ b/api/v1alpha1/managedcluster_types.go @@ -106,6 +106,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. diff --git a/api/v1alpha1/management_types.go b/api/v1alpha1/management_types.go index 0763ac01..7c036ab3 100644 --- a/api/v1alpha1/management_types.go +++ b/api/v1alpha1/management_types.go @@ -75,37 +75,37 @@ 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"` // Release indicates the current Release object. Release string `json:"release,omitempty"` + // AvailableProviders holds all CAPI providers available along with + // their exact compatibility versions if specified in ProviderTemplates on the Management cluster. + AvailableProviders ProvidersTupled `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..e35cbd7f 100644 --- a/api/v1alpha1/providertemplate_types.go +++ b/api/v1alpha1/providertemplate_types.go @@ -15,24 +15,70 @@ package v1alpha1 import ( + "fmt" + + "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"` + // Represents required CAPI providers with exact compatibility versions set. Should be set if not present in the Helm chart metadata. + Providers ProvidersTupled `json:"providers,omitempty"` } // ProviderTemplateStatus defines the observed state of ProviderTemplate type ProviderTemplateStatus struct { + // Compatible CAPI provider version in the SemVer format. + CAPIVersion string `json:"capiVersion,omitempty"` + // Providers represent exposed CAPI providers with exact compatibility versions set. + Providers ProvidersTupled `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 { + var err error + t.Status.Providers.BootstrapProviders, err = parseProviders(t, bootstrapProvidersType, annotations, semver.NewVersion) + if err != nil { + return fmt.Errorf("failed to parse ProviderTemplate bootstrap providers: %v", err) + } + + t.Status.Providers.ControlPlaneProviders, err = parseProviders(t, controlPlaneProvidersType, annotations, semver.NewVersion) + if err != nil { + return fmt.Errorf("failed to parse ProviderTemplate controlPlane providers: %v", err) + } + + t.Status.Providers.InfrastructureProviders, err = parseProviders(t, infrastructureProvidersType, annotations, semver.NewVersion) + if err != nil { + return fmt.Errorf("failed to parse ProviderTemplate infrastructure providers: %v", err) + } + + return nil +} + +// GetSpecProviders returns .spec.providers of the Template. +func (t *ProviderTemplate) GetSpecProviders() ProvidersTupled { + return t.Spec.Providers +} + +// GetStatusProviders returns .status.providers of the Template. +func (t *ProviderTemplate) GetStatusProviders() ProvidersTupled { + return t.Status.Providers +} + +// GetHelmSpec returns .spec.helm of the Template. +func (t *ProviderTemplate) GetHelmSpec() *HelmSpec { + return &t.Spec.Helm } -func (t *ProviderTemplate) GetStatus() *TemplateStatusCommon { +// GetCommonStatus returns common status of the Template. +func (t *ProviderTemplate) GetCommonStatus() *TemplateStatusCommon { return &t.Status.TemplateStatusCommon } diff --git a/api/v1alpha1/servicetemplate_types.go b/api/v1alpha1/servicetemplate_types.go index 73148619..10c3152f 100644 --- a/api/v1alpha1/servicetemplate_types.go +++ b/api/v1alpha1/servicetemplate_types.go @@ -15,6 +15,8 @@ package v1alpha1 import ( + "strings" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -22,19 +24,71 @@ const ServiceTemplateKind = "ServiceTemplate" // 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"` + // Represents required CAPI providers. 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 { + // Constraint describing compatible K8S versions of the cluster set in the SemVer format. + KubertenesConstraint string `json:"k8sConstraint,omitempty"` + // Represents exposed CAPI providers. + 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 +} + +// GetHelmSpec returns .spec.helm of the Template. +func (t *ServiceTemplate) GetHelmSpec() *HelmSpec { + return &t.Spec.Helm } -func (t *ServiceTemplate) GetStatus() *TemplateStatusCommon { +// GetCommonStatus returns common status of the Template. +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..5ea92cca 100644 --- a/api/v1alpha1/templates_common.go +++ b/api/v1alpha1/templates_common.go @@ -15,6 +15,10 @@ package v1alpha1 import ( + "errors" + "fmt" + "strings" + helmcontrollerv2 "github.com/fluxcd/helm-controller/api/v2" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" ) @@ -28,15 +32,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 +45,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 +76,75 @@ 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 +) + +func parseProviders[T any](providersGetter interface { + GetSpecProviders() ProvidersTupled + GetStatusProviders() ProvidersTupled +}, typ providersType, annotations map[string]string, validationFn func(string) (T, error), +) ([]ProviderTuple, error) { + pspec, pstatus, anno := getProvidersSpecStatusAnno(providersGetter, typ) + if len(pspec) > 0 { + return pstatus, nil + } + + providers := annotations[anno] + if len(providers) == 0 { + return []ProviderTuple{}, nil + } + + var ( + splitted = strings.Split(providers, ",") + merr error + ) + + pstatus = make([]ProviderTuple, 0, len(splitted)) + + for _, v := range splitted { + nVerOrC := strings.SplitN(v, " ", 1) + if len(nVerOrC) == 0 { // BCE (bound check elimination) + continue + } + + n := ProviderTuple{Name: nVerOrC[0]} + if len(nVerOrC) < 2 { + pstatus = append(pstatus, n) + continue + } + + ver := strings.TrimSpace(nVerOrC[1]) + if _, err := validationFn(ver); err != nil { // validation + merr = errors.Join(merr, fmt.Errorf("failed to parse version %s in the %s: %v", ver, v, err)) + continue + } + + n.VersionOrContraint = ver + pstatus = append(pstatus, n) + } + + return pstatus, merr +} + +func getProvidersSpecStatusAnno(providersGetter interface { + GetSpecProviders() ProvidersTupled + GetStatusProviders() ProvidersTupled +}, typ providersType, +) (spec, status []ProviderTuple, annotation string) { + switch typ { + case bootstrapProvidersType: + return providersGetter.GetSpecProviders().BootstrapProviders, providersGetter.GetStatusProviders().BootstrapProviders, ChartAnnotationBootstrapProviders + case controlPlaneProvidersType: + return providersGetter.GetSpecProviders().ControlPlaneProviders, providersGetter.GetStatusProviders().ControlPlaneProviders, ChartAnnotationControlPlaneProviders + case infrastructureProvidersType: + return providersGetter.GetSpecProviders().InfrastructureProviders, providersGetter.GetStatusProviders().InfrastructureProviders, ChartAnnotationInfraProviders + default: + return []ProviderTuple{}, []ProviderTuple{}, "" + } +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index a9a14f7a..b62a24b5 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) } @@ -614,7 +616,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)) @@ -622,6 +623,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. @@ -728,7 +730,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. @@ -744,6 +747,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) } @@ -757,6 +761,21 @@ func (in *ProviderTemplateStatus) DeepCopy() *ProviderTemplateStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderTuple) DeepCopyInto(out *ProviderTuple) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderTuple. +func (in *ProviderTuple) DeepCopy() *ProviderTuple { + if in == nil { + return nil + } + out := new(ProviderTuple) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Providers) DeepCopyInto(out *Providers) { *out = *in @@ -787,6 +806,36 @@ 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 *ProvidersTupled) DeepCopyInto(out *ProvidersTupled) { + *out = *in + if in.InfrastructureProviders != nil { + in, out := &in.InfrastructureProviders, &out.InfrastructureProviders + *out = make([]ProviderTuple, len(*in)) + copy(*out, *in) + } + if in.BootstrapProviders != nil { + in, out := &in.BootstrapProviders, &out.BootstrapProviders + *out = make([]ProviderTuple, len(*in)) + copy(*out, *in) + } + if in.ControlPlaneProviders != nil { + in, out := &in.ControlPlaneProviders, &out.ControlPlaneProviders + *out = make([]ProviderTuple, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProvidersTupled. +func (in *ProvidersTupled) DeepCopy() *ProvidersTupled { + if in == nil { + return nil + } + out := new(ProvidersTupled) + 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 @@ -1011,7 +1060,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. @@ -1027,6 +1077,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) } @@ -1210,27 +1261,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) @@ -1241,7 +1274,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 cd5cb07f..960f1c1e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -187,7 +187,6 @@ func main() { templateReconciler := controller.TemplateReconciler{ Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), SystemNamespace: currentNamespace, DefaultRegistryConfig: helm.DefaultRegistryConfig{ URL: defaultRegistryURL, diff --git a/go.mod b/go.mod index 6d52d462..8ab17d8d 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/Mirantis/hmc go 1.22.7 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.1.0 @@ -37,7 +38,6 @@ require ( github.com/BurntSushi/toml v1.4.0 // 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/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect diff --git a/internal/controller/managedcluster_controller.go b/internal/controller/managedcluster_controller.go index a2b6e8e1..f10b6804 100644 --- a/internal/controller/managedcluster_controller.go +++ b/internal/controller/managedcluster_controller.go @@ -45,7 +45,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" hmc "github.com/Mirantis/hmc/api/v1alpha1" @@ -374,7 +373,7 @@ func (r *ManagedClusterReconciler) Update(ctx context.Context, l logr.Logger, ma // updateServices reconciles services provided in ManagedCluster.Spec.Services. // TODO(https://github.com/Mirantis/hmc/issues/361): Set status to ManagedCluster object at appropriate places. func (r *ManagedClusterReconciler) updateServices(ctx context.Context, mc *hmc.ManagedCluster) (ctrl.Result, error) { - l := log.FromContext(ctx).WithValues("ManagedClusterController", fmt.Sprintf("%s/%s", mc.Namespace, mc.Name)) + l := ctrl.LoggerFrom(ctx) opts := []sveltos.HelmChartOpts{} // NOTE: The Profile object will be updated with no helm @@ -604,7 +603,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 } @@ -627,13 +626,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.ProviderTuple, error) { template := &hmc.ClusterTemplate{} templateRef := client.ObjectKey{Name: templateName, Namespace: templateNamespace} if err := r.Get(ctx, templateRef, template); err != nil { ctrl.LoggerFrom(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 72f2edde..96f77e23 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", }, }, }, @@ -154,8 +152,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 efed06f6..ebbc2c03 100644 --- a/internal/controller/management_controller.go +++ b/internal/controller/management_controller.go @@ -105,7 +105,7 @@ func (r *ManagementReconciler) Update(ctx context.Context, management *hmc.Manag } var errs error - detectedProviders := hmc.Providers{} + detectedProviders := hmc.ProvidersTupled{} detectedComponents := make(map[string]hmc.ComponentStatus) err := r.enableAdditionalComponents(ctx, management) @@ -313,6 +313,7 @@ func wrappedComponents(mgmt *hmc.Management, release *hmc.Release) ([]component, } hmcComp.Config = hmcConfig components = append(components, hmcComp) + capiComp := component{ Component: mgmt.Spec.Core.CAPI, helmReleaseName: hmc.CoreCAPIName, dependsOn: []meta.NamespacedObjectReference{{Name: hmc.CoreHMCName}}, @@ -336,6 +337,7 @@ func wrappedComponents(mgmt *hmc.Management, release *hmc.Release) ([]component, c.targetNamespace = hmc.ProviderSveltosTargetNamespace c.createNamespace = hmc.ProviderSveltosCreateNamespace } + components = append(components, c) } @@ -410,10 +412,10 @@ func (r *ManagementReconciler) enableAdditionalComponents(ctx context.Context, m func updateComponentsStatus( components map[string]hmc.ComponentStatus, - providers *hmc.Providers, + providers *hmc.ProvidersTupled, componentName string, templateName string, - templateProviders hmc.Providers, + templateProviders hmc.ProvidersTupled, err string, ) { components[componentName] = hmc.ComponentStatus{ 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 8736e148..49246638 100644 --- a/internal/controller/release_controller.go +++ b/internal/controller/release_controller.go @@ -110,9 +110,9 @@ func (r *ReleaseReconciler) ensureManagement(ctx context.Context) error { if err != nil { return err } - if err := mgmtObj.Spec.SetProvidersDefaults(); err != nil { - return err - } + + mgmtObj.Spec.Providers = hmc.GetDefaultProviders() + getter := helm.NewMemoryRESTClientGetter(r.Config, r.RESTMapper()) actionConfig := new(action.Configuration) err = actionConfig.Init(getter, r.SystemNamespace, "secret", l.Info) diff --git a/internal/controller/template_controller.go b/internal/controller/template_controller.go index da312028..8b1cf979 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" sourcev1 "github.com/fluxcd/source-controller/api/v1" @@ -26,7 +25,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" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -38,10 +36,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 DefaultRegistryConfig helm.DefaultRegistryConfig @@ -65,26 +62,26 @@ func (r *ClusterTemplateReconciler) Reconcile(ctx context.Context, req ctrl.Requ l := ctrl.LoggerFrom(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 := ctrl.LoggerFrom(ctx).WithValues("ServiceTemplateReconciler", req.NamespacedName) + l := ctrl.LoggerFrom(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 @@ -96,44 +93,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 := ctrl.LoggerFrom(ctx).WithValues("ProviderTemplateReconciler", req.NamespacedName) + l := ctrl.LoggerFrom(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 := ctrl.LoggerFrom(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 @@ -189,19 +187,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") @@ -210,52 +212,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 == "" @@ -266,8 +242,7 @@ func (r *TemplateReconciler) updateStatus(ctx context.Context, template Template 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 @@ -279,10 +254,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{ { @@ -292,21 +269,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/templatechain_controller.go b/internal/controller/templatechain_controller.go index ca2406e7..c20bc20b 100644 --- a/internal/controller/templatechain_controller.go +++ b/internal/controller/templatechain_controller.go @@ -25,7 +25,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" hmc "github.com/Mirantis/hmc/api/v1alpha1" ) @@ -46,8 +45,8 @@ type ServiceTemplateChainReconciler struct { TemplateChainReconciler } -// TemplateChain is the interface defining a list of methods to interact with templatechains -type TemplateChain interface { +// templateChain is the interface defining a list of methods to interact with *templatechains +type templateChain interface { client.Object Kind() string TemplateKind() string @@ -55,7 +54,7 @@ type TemplateChain interface { } func (r *ClusterTemplateChainReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - l := log.FromContext(ctx).WithValues("ClusterTemplateChainController", req.NamespacedName) + l := ctrl.LoggerFrom(ctx) l.Info("Reconciling ClusterTemplateChain") clusterTemplateChain := &hmc.ClusterTemplateChain{} @@ -72,7 +71,7 @@ func (r *ClusterTemplateChainReconciler) Reconcile(ctx context.Context, req ctrl } func (r *ServiceTemplateChainReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - l := log.FromContext(ctx).WithValues("ServiceTemplateChainReconciler", req.NamespacedName) + l := ctrl.LoggerFrom(ctx) l.Info("Reconciling ServiceTemplateChain") serviceTemplateChain := &hmc.ServiceTemplateChain{} @@ -88,17 +87,18 @@ func (r *ServiceTemplateChainReconciler) Reconcile(ctx context.Context, req ctrl return r.ReconcileTemplateChain(ctx, serviceTemplateChain) } -func (r *TemplateChainReconciler) ReconcileTemplateChain(ctx context.Context, templateChain TemplateChain) (ctrl.Result, error) { - l := log.FromContext(ctx) +func (r *TemplateChainReconciler) ReconcileTemplateChain(ctx context.Context, templateChain templateChain) (ctrl.Result, error) { + l := ctrl.LoggerFrom(ctx) systemTemplates, managedTemplates, err := getCurrentTemplates(ctx, r.Client, templateChain.TemplateKind(), r.SystemNamespace, templateChain.GetNamespace(), templateChain.GetName()) if err != nil { return ctrl.Result{}, fmt.Errorf("failed to get current templates: %v", err) } - var errs error - - keepTemplate := make(map[string]bool) + var ( + errs error + keepTemplate = make(map[string]struct{}, len(templateChain.GetSpec().SupportedTemplates)) + ) for _, supportedTemplate := range templateChain.GetSpec().SupportedTemplates { meta := metav1.ObjectMeta{ Name: supportedTemplate.Name, @@ -108,7 +108,7 @@ func (r *TemplateChainReconciler) ReconcileTemplateChain(ctx context.Context, te HMCManagedByChainLabelKey: templateChain.GetName(), }, } - keepTemplate[supportedTemplate.Name] = true + keepTemplate[supportedTemplate.Name] = struct{}{} source, found := systemTemplates[supportedTemplate.Name] if !found { @@ -116,13 +116,11 @@ func (r *TemplateChainReconciler) ReconcileTemplateChain(ctx context.Context, te continue } - templateSpec := hmc.TemplateSpecCommon{ - Helm: hmc.HelmSpec{ - ChartRef: &helmcontrollerv2.CrossNamespaceSourceReference{ - Kind: sourcev1.HelmChartKind, - Name: source.GetSpec().Helm.ChartName, - Namespace: r.SystemNamespace, - }, + helmSpec := hmc.HelmSpec{ + ChartRef: &helmcontrollerv2.CrossNamespaceSourceReference{ + Kind: sourcev1.HelmChartKind, + Name: source.GetHelmSpec().ChartName, + Namespace: r.SystemNamespace, }, } @@ -130,42 +128,47 @@ func (r *TemplateChainReconciler) ReconcileTemplateChain(ctx context.Context, te switch templateChain.Kind() { case hmc.ClusterTemplateChainKind: target = &hmc.ClusterTemplate{ObjectMeta: meta, Spec: hmc.ClusterTemplateSpec{ - TemplateSpecCommon: templateSpec, + Helm: helmSpec, }} case hmc.ServiceTemplateChainKind: target = &hmc.ServiceTemplate{ObjectMeta: meta, Spec: hmc.ServiceTemplateSpec{ - TemplateSpecCommon: templateSpec, + Helm: helmSpec, }} default: return ctrl.Result{}, fmt.Errorf("invalid TemplateChain kind. Supported kinds are %s and %s", hmc.ClusterTemplateChainKind, hmc.ServiceTemplateChainKind) } - err := r.Create(ctx, target) - if err == nil { - l.Info(fmt.Sprintf("%s was successfully created", templateChain.TemplateKind()), "namespace", templateChain.GetNamespace(), "name", supportedTemplate) + + if err := r.Create(ctx, target); err == nil { + l.Info(fmt.Sprintf("%s was successfully created", templateChain.TemplateKind()), "template namespace", templateChain.GetNamespace(), "template name", supportedTemplate.Name) continue } + if !apierrors.IsAlreadyExists(err) { errs = errors.Join(errs, err) } } + for _, template := range managedTemplates { - if !keepTemplate[template.GetName()] { - l.Info(fmt.Sprintf("Deleting %s", templateChain.TemplateKind()), "namespace", templateChain.GetNamespace(), "name", template.GetName()) - err := r.Delete(ctx, template) - if err == nil { - l.Info(fmt.Sprintf("%s was deleted", templateChain.TemplateKind()), "namespace", templateChain.GetNamespace(), "name", template.GetName()) - continue - } - if !apierrors.IsNotFound(err) { - errs = errors.Join(errs, err) - } + templateName := template.GetName() + if _, keep := keepTemplate[templateName]; keep { + continue } + + ll := l.WithValues("template kind", templateChain.TemplateKind(), "template namespace", templateChain.GetNamespace(), "template name", templateName) + ll.Info("Deleting Template") + + if err := r.Delete(ctx, template); client.IgnoreNotFound(err) != nil { + errs = errors.Join(errs, err) + continue + } + + ll.Info("Template has been deleted") } return ctrl.Result{}, nil } -func getCurrentTemplates(ctx context.Context, cl client.Client, templateKind, systemNamespace, targetNamespace, templateChainName string) (map[string]Template, []Template, error) { - var templates []Template +func getCurrentTemplates(ctx context.Context, cl client.Client, templateKind, systemNamespace, targetNamespace, templateChainName string) (systemTemplates map[string]templateCommon, managedTemplates []templateCommon, _ error) { + var templates []templateCommon switch templateKind { case hmc.ClusterTemplateKind: @@ -189,21 +192,23 @@ func getCurrentTemplates(ctx context.Context, cl client.Client, templateKind, sy default: return nil, nil, fmt.Errorf("invalid Template kind. Supported kinds are %s and %s", hmc.ClusterTemplateKind, hmc.ServiceTemplateKind) } - systemTemplates := make(map[string]Template) - var managedTemplates []Template + systemTemplates = make(map[string]templateCommon, len(templates)) + managedTemplates = make([]templateCommon, 0, len(templates)) for _, template := range templates { if template.GetNamespace() == systemNamespace { systemTemplates[template.GetName()] = template continue } + labels := template.GetLabels() if template.GetNamespace() == targetNamespace && - labels[hmc.HMCManagedLabelKey] == "true" && + labels[hmc.HMCManagedLabelKey] == hmc.HMCManagedLabelValue && labels[HMCManagedByChainLabelKey] == templateChainName { managedTemplates = append(managedTemplates, template) } } + return systemTemplates, managedTemplates, nil } diff --git a/internal/controller/templatemanagement_controller.go b/internal/controller/templatemanagement_controller.go index 0384f698..40f69a2b 100644 --- a/internal/controller/templatemanagement_controller.go +++ b/internal/controller/templatemanagement_controller.go @@ -142,8 +142,8 @@ func getNamespacedName(namespace, name string) string { return fmt.Sprintf("%s/%s", namespace, name) } -func (r *TemplateManagementReconciler) getCurrentTemplateChains(ctx context.Context, templateChainKind string) (map[string]TemplateChain, []TemplateChain, error) { - var templateChains []TemplateChain +func (r *TemplateManagementReconciler) getCurrentTemplateChains(ctx context.Context, templateChainKind string) (map[string]templateChain, []templateChain, error) { + var templateChains []templateChain switch templateChainKind { case hmc.ClusterTemplateChainKind: ctChainList := &hmc.ClusterTemplateChainList{} @@ -167,17 +167,21 @@ func (r *TemplateManagementReconciler) getCurrentTemplateChains(ctx context.Cont return nil, nil, fmt.Errorf("invalid TemplateChain kind. Supported kinds are %s and %s", hmc.ClusterTemplateChainKind, hmc.ServiceTemplateChainKind) } - systemTemplateChains := make(map[string]TemplateChain) - var managedTemplateChains []TemplateChain + var ( + systemTemplateChains = make(map[string]templateChain, len(templateChains)) + managedTemplateChains = make([]templateChain, 0, len(templateChains)) + ) for _, chain := range templateChains { if chain.GetNamespace() == r.SystemNamespace { systemTemplateChains[chain.GetName()] = chain continue } + if chain.GetLabels()[hmc.HMCManagedLabelKey] == hmc.HMCManagedLabelValue { managedTemplateChains = append(managedTemplateChains, chain) } } + return systemTemplateChains, managedTemplateChains, nil } @@ -199,23 +203,27 @@ func getTargetNamespaces(ctx context.Context, cl client.Client, targetNamespaces } } - namespaces := &corev1.NamespaceList{} - listOpts := &client.ListOptions{} - if selector.String() != "" { - listOpts = &client.ListOptions{LabelSelector: selector} + var ( + namespaces = new(corev1.NamespaceList) + listOpts = new(client.ListOptions) + ) + if !selector.Empty() { + listOpts.LabelSelector = selector } - err = cl.List(ctx, namespaces, listOpts) - if err != nil { - return []string{}, err + + if err := cl.List(ctx, namespaces, listOpts); err != nil { + return nil, err } + result := make([]string, len(namespaces.Items)) for i, ns := range namespaces.Items { result[i] = ns.Name } + return result, nil } -func (r *TemplateManagementReconciler) createTemplateChain(ctx context.Context, source TemplateChain, targetNamespace string) error { +func (r *TemplateManagementReconciler) createTemplateChain(ctx context.Context, source templateChain, targetNamespace string) error { l := ctrl.LoggerFrom(ctx) meta := metav1.ObjectMeta{ @@ -225,7 +233,7 @@ func (r *TemplateManagementReconciler) createTemplateChain(ctx context.Context, hmc.HMCManagedLabelKey: hmc.HMCManagedLabelValue, }, } - var target TemplateChain + var target templateChain switch source.Kind() { case hmc.ClusterTemplateChainKind: target = &hmc.ClusterTemplateChain{ObjectMeta: meta, Spec: *source.GetSpec()} @@ -244,7 +252,7 @@ func (r *TemplateManagementReconciler) createTemplateChain(ctx context.Context, return nil } -func (r *TemplateManagementReconciler) deleteTemplateChain(ctx context.Context, chain TemplateChain) error { +func (r *TemplateManagementReconciler) deleteTemplateChain(ctx context.Context, chain templateChain) error { l := ctrl.LoggerFrom(ctx) err := r.Delete(ctx, chain) diff --git a/internal/telemetry/tracker.go b/internal/telemetry/tracker.go index a9b578bd..83d9b858 100644 --- a/internal/telemetry/tracker.go +++ b/internal/telemetry/tracker.go @@ -93,9 +93,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 79cc2b2a..113a89f6 100644 --- a/internal/webhook/managedcluster_webhook.go +++ b/internal/webhook/managedcluster_webhook.go @@ -29,7 +29,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 { @@ -170,11 +169,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, requiredProviders []v1alpha1.ProviderTuple) (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..32557cfe 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.ProvidersTupled{ + InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "aws"}}, + BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}}, + ControlPlaneProviders: []v1alpha1.ProviderTuple{{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.ProvidersTupled{ + InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "aws"}}, + BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}}, }), ), template.NewClusterTemplate( template.WithName(testTemplateName), - template.WithProvidersStatus(v1alpha1.Providers{ - InfrastructureProviders: []string{"azure"}, - BootstrapProviders: []string{"k0s"}, - ControlPlaneProviders: []string{"k0s"}, + template.WithProvidersStatus(v1alpha1.ProvidersTupled{ + InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "azure"}}, + BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}}, + ControlPlaneProviders: []v1alpha1.ProviderTuple{{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.ProvidersTupled{ + InfrastructureProviders: []v1alpha1.ProviderTuple{{Name: "aws"}}, + BootstrapProviders: []v1alpha1.ProviderTuple{{Name: "k0s"}}, + ControlPlaneProviders: []v1alpha1.ProviderTuple{{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..4dc8aadd 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,62 @@ 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. + description: Providers represent required CAPI providers with constrainted + compatibility versions set. 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 either an exact + or constrainted version in the SemVer format. items: - type: string + description: Represents name of the provider with either an + exact or constrainted version in the SemVer format. + properties: + name: + description: Name of the provider. + type: string + versionOrContraint: + description: Compatibility restriction in the SemVer format + (exact or constrainted version) + 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 either + an exact or constrainted version in the SemVer format. items: - type: string + description: Represents name of the provider with either an + exact or constrainted version in the SemVer format. + properties: + name: + description: Name of the provider. + type: string + versionOrContraint: + description: Compatibility restriction in the SemVer format + (exact or constrainted version) + type: string + type: object type: array infrastructure: - description: InfrastructureProviders is the list of CAPI infrastructure - providers + description: List of CAPI infrastructure providers with either + an exact or constrainted version in the SemVer format. items: - type: string + description: Represents name of the provider with either an + exact or constrainted version in the SemVer format. + properties: + name: + description: Name of the provider. + type: string + versionOrContraint: + description: Compatibility restriction in the SemVer format + (exact or constrainted version) + type: string + type: object type: array type: object required: @@ -174,31 +208,65 @@ spec: description: description: Description contains information about the template. type: string + k8sVersion: + description: Compatible K8S version of the cluster set in the SemVer + format. + type: string observedGeneration: description: ObservedGeneration is the last observed generation. format: int64 type: integer providers: - description: Providers represent required/exposed CAPI providers depending - on the template type. + description: Providers represent exposed CAPI providers with constrainted + compatibility versions set. properties: bootstrap: - description: BootstrapProviders is the list of CAPI bootstrap - providers + description: List of CAPI bootstrap providers with either an exact + or constrainted version in the SemVer format. items: - type: string + description: Represents name of the provider with either an + exact or constrainted version in the SemVer format. + properties: + name: + description: Name of the provider. + type: string + versionOrContraint: + description: Compatibility restriction in the SemVer format + (exact or constrainted version) + 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 either + an exact or constrainted version in the SemVer format. items: - type: string + description: Represents name of the provider with either an + exact or constrainted version in the SemVer format. + properties: + name: + description: Name of the provider. + type: string + versionOrContraint: + description: Compatibility restriction in the SemVer format + (exact or constrainted version) + type: string + type: object type: array infrastructure: - description: InfrastructureProviders is the list of CAPI infrastructure - providers + description: List of CAPI infrastructure providers with either + an exact or constrainted version in the SemVer format. items: - type: string + description: Represents name of the provider with either an + exact or constrainted version in the SemVer format. + properties: + name: + description: Name of the provider. + type: string + versionOrContraint: + description: Compatibility restriction in the SemVer format + (exact or constrainted version) + 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 0fd68ab4..1f20861e 100644 --- a/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml +++ b/templates/provider/hmc/templates/crds/hmc.mirantis.com_managedclusters.yaml @@ -171,6 +171,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 a0971a3c..04c98a06 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,57 @@ 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 exact compatibility 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 either an exact + or constrainted version in the SemVer format. items: - type: string + description: Represents name of the provider with either an + exact or constrainted version in the SemVer format. + properties: + name: + description: Name of the provider. + type: string + versionOrContraint: + description: Compatibility restriction in the SemVer format + (exact or constrainted version) + 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 either + an exact or constrainted version in the SemVer format. items: - type: string + description: Represents name of the provider with either an + exact or constrainted version in the SemVer format. + properties: + name: + description: Name of the provider. + type: string + versionOrContraint: + description: Compatibility restriction in the SemVer format + (exact or constrainted version) + type: string + type: object type: array infrastructure: - description: InfrastructureProviders is the list of CAPI infrastructure - providers + description: List of CAPI infrastructure providers with either + an exact or constrainted version in the SemVer format. items: - type: string + description: Represents name of the provider with either an + exact or constrainted version in the SemVer format. + properties: + name: + description: Name of the provider. + type: string + versionOrContraint: + description: Compatibility restriction in the SemVer format + (exact or constrainted version) + 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..8442a2e1 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 @@ -104,27 +107,56 @@ spec: rule: (has(self.chartName) && !has(self.chartRef)) || (!has(self.chartName) && has(self.chartRef)) providers: - description: |- - Providers represent required/exposed CAPI providers depending on the template type. - Should be set if not present in the Helm chart metadata. + description: Represents required CAPI providers with exact compatibility + versions set. 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 either an exact + or constrainted version in the SemVer format. items: - type: string + description: Represents name of the provider with either an + exact or constrainted version in the SemVer format. + properties: + name: + description: Name of the provider. + type: string + versionOrContraint: + description: Compatibility restriction in the SemVer format + (exact or constrainted version) + 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 either + an exact or constrainted version in the SemVer format. items: - type: string + description: Represents name of the provider with either an + exact or constrainted version in the SemVer format. + properties: + name: + description: Name of the provider. + type: string + versionOrContraint: + description: Compatibility restriction in the SemVer format + (exact or constrainted version) + type: string + type: object type: array infrastructure: - description: InfrastructureProviders is the list of CAPI infrastructure - providers + description: List of CAPI infrastructure providers with either + an exact or constrainted version in the SemVer format. items: - type: string + description: Represents name of the provider with either an + exact or constrainted version in the SemVer format. + properties: + name: + description: Name of the provider. + type: string + versionOrContraint: + description: Compatibility restriction in the SemVer format + (exact or constrainted version) + type: string + type: object type: array type: object required: @@ -136,6 +168,9 @@ spec: status: description: ProviderTemplateStatus defines the observed state of ProviderTemplate properties: + capiVersion: + description: Compatible CAPI provider version in the SemVer format. + type: string chartRef: description: |- ChartRef is a reference to a source controller resource containing the @@ -179,26 +214,56 @@ spec: format: int64 type: integer providers: - description: Providers represent required/exposed CAPI providers depending - on the template type. + description: Providers represent exposed CAPI providers with exact + compatibility versions set. properties: bootstrap: - description: BootstrapProviders is the list of CAPI bootstrap - providers + description: List of CAPI bootstrap providers with either an exact + or constrainted version in the SemVer format. items: - type: string + description: Represents name of the provider with either an + exact or constrainted version in the SemVer format. + properties: + name: + description: Name of the provider. + type: string + versionOrContraint: + description: Compatibility restriction in the SemVer format + (exact or constrainted version) + 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 either + an exact or constrainted version in the SemVer format. items: - type: string + description: Represents name of the provider with either an + exact or constrainted version in the SemVer format. + properties: + name: + description: Name of the provider. + type: string + versionOrContraint: + description: Compatibility restriction in the SemVer format + (exact or constrainted version) + type: string + type: object type: array infrastructure: - description: InfrastructureProviders is the list of CAPI infrastructure - providers + description: List of CAPI infrastructure providers with either + an exact or constrainted version in the SemVer format. items: - type: string + description: Represents name of the provider with either an + exact or constrainted version in the SemVer format. + properties: + name: + description: Name of the provider. + type: string + versionOrContraint: + description: Compatibility restriction in the SemVer format + (exact or constrainted version) + 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..465f5723 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,10 +103,13 @@ 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. - Should be set if not present in the Helm chart metadata. + description: Represents required CAPI providers. Should be set if + not present in the Helm chart metadata. properties: bootstrap: description: BootstrapProviders is the list of CAPI bootstrap @@ -174,13 +177,16 @@ spec: description: description: Description contains information about the template. type: string + k8sConstraint: + description: Constraint describing compatible K8S versions of the + cluster set in the SemVer format. + type: string observedGeneration: description: ObservedGeneration is the last observed generation. format: int64 type: integer providers: - description: Providers represent required/exposed CAPI providers depending - on the template type. + description: Represents exposed CAPI providers. properties: bootstrap: description: BootstrapProviders is the list of CAPI bootstrap diff --git a/test/objects/management/management.go b/test/objects/management/management.go index 26a31899..98c5ade1 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.ProvidersTupled) 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 f3184be3..6fa1e5bd 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,108 +29,136 @@ 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 ManagedByHMC() Opt { - return func(t *Template) { - if t.Labels == nil { - t.Labels = make(map[string]string) + return func(template Template) { + labels := template.GetLabels() + if labels == nil { + labels = make(map[string]string) } - t.Labels[v1alpha1.HMCManagedLabelKey] = v1alpha1.HMCManagedLabelValue - } -} + labels[v1alpha1.HMCManagedLabelKey] = v1alpha1.HMCManagedLabelValue -func WithHelmSpec(helmSpec v1alpha1.HelmSpec) Opt { - return func(t *Template) { - t.Spec.Helm = helmSpec + template.SetLabels(labels) } } -func WithProviders(providers v1alpha1.Providers) Opt { - return func(t *Template) { - t.Spec.Providers = providers +func WithHelmSpec(helmSpec v1alpha1.HelmSpec) Opt { + 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.ProvidersTupled](providers T) Opt { + return func(t Template) { + switch v := t.(type) { + case *v1alpha1.ClusterTemplate: + var ok bool + v.Status.Providers, ok = any(providers).(v1alpha1.ProvidersTupled) + if !ok { + panic(fmt.Sprintf("unexpected type %T", providers)) + } + case *v1alpha1.ProviderTemplate: + var ok bool + v.Status.Providers, ok = any(providers).(v1alpha1.ProvidersTupled) + 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), } }