Skip to content

Commit

Permalink
Cleanup component dependencies when removed
Browse files Browse the repository at this point in the history
* if component is removed from management object
  the related helmreleases, providertemplates and
  helmcharts are deleted now
* small code amends
* test coverage for the mgmt-ctrl raised
  from 4.4% to 45.6%

Closes #553
  • Loading branch information
zerospiel authored and Kshatrix committed Nov 7, 2024
1 parent 252017c commit 2af64cc
Show file tree
Hide file tree
Showing 4 changed files with 332 additions and 82 deletions.
196 changes: 124 additions & 72 deletions internal/controller/management_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,6 @@ import (
"github.com/Mirantis/hmc/internal/utils/status"
)

// Those are only needed for the initial installation
var enforcedValues = map[string]any{
"controller": map[string]any{
"createManagement": false,
"createTemplateManagement": false,
"createRelease": false,
},
}

// ManagementReconciler reconciles a Management object
type ManagementReconciler struct {
client.Client
Expand Down Expand Up @@ -92,101 +83,139 @@ func (r *ManagementReconciler) Update(ctx context.Context, management *hmc.Manag

if controllerutil.AddFinalizer(management, hmc.ManagementFinalizer) {
if err := r.Client.Update(ctx, management); err != nil {
l.Error(err, "Failed to update Management finalizers")
l.Error(err, "failed to update Management finalizers")
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}

if err := r.ensureTemplateManagement(ctx, management); err != nil {
l.Error(err, "Failed to ensure TemplateManagement is created")
if err := r.cleanupRemovedComponents(ctx, management); err != nil {
l.Error(err, "failed to cleanup removed components")
return ctrl.Result{}, err
}

release := &hmc.Release{}
if err := r.Client.Get(ctx, client.ObjectKey{Name: management.Spec.Release}, release); err != nil {
l.Error(err, "failed to get Release object")
if err := r.ensureTemplateManagement(ctx, management); err != nil {
l.Error(err, "failed to ensure TemplateManagement is created")
return ctrl.Result{}, err
}

var (
errs error

detectedProviders = hmc.Providers{}
detectedComponents = make(map[string]hmc.ComponentStatus)
detectedContracts = make(map[string]hmc.CompatibilityContracts)
)

err := r.enableAdditionalComponents(ctx, management)
if err != nil {
if err := r.enableAdditionalComponents(ctx, management); err != nil { // TODO (zerospiel): i wonder, do we need to reflect these changes and changes from the `wrappedComponents` in the spec?
l.Error(err, "failed to enable additional HMC components")
return ctrl.Result{}, err
}

components, err := wrappedComponents(management, release)
components, err := getWrappedComponents(ctx, r.Client, management)
if err != nil {
l.Error(err, "failed to wrap HMC components")
return ctrl.Result{}, err
}

var (
errs error

statusAccumulator = &mgmtStatusAccumulator{
providers: hmc.Providers{},
components: make(map[string]hmc.ComponentStatus),
compatibilityContracts: make(map[string]hmc.CompatibilityContracts),
}
)
for _, component := range components {
template := &hmc.ProviderTemplate{}
err := r.Get(ctx, client.ObjectKey{
Name: component.Template,
}, template)
if err != nil {
l.V(1).Info("reconciling components", "component", component)
template := new(hmc.ProviderTemplate)
if err := r.Get(ctx, client.ObjectKey{Name: component.Template}, template); err != nil {
errMsg := fmt.Sprintf("Failed to get ProviderTemplate %s: %s", component.Template, err)
updateComponentsStatus(detectedComponents, &detectedProviders, detectedContracts, component.helmReleaseName, component.Template, template.Status.Providers, template.Status.CAPIContracts, errMsg)
updateComponentsStatus(statusAccumulator, component, nil, errMsg)
errs = errors.Join(errs, errors.New(errMsg))

continue
}

if !template.Status.Valid {
errMsg := fmt.Sprintf("Template %s is not marked as valid", component.Template)
updateComponentsStatus(detectedComponents, &detectedProviders, detectedContracts, component.helmReleaseName, component.Template, template.Status.Providers, template.Status.CAPIContracts, errMsg)
updateComponentsStatus(statusAccumulator, component, nil, errMsg)
errs = errors.Join(errs, errors.New(errMsg))

continue
}

_, _, err = helm.ReconcileHelmRelease(ctx, r.Client, component.helmReleaseName, r.SystemNamespace, helm.ReconcileHelmReleaseOpts{
if _, _, err := helm.ReconcileHelmRelease(ctx, r.Client, component.helmReleaseName, r.SystemNamespace, helm.ReconcileHelmReleaseOpts{
Values: component.Config,
ChartRef: template.Status.ChartRef,
DependsOn: component.dependsOn,
TargetNamespace: component.targetNamespace,
CreateNamespace: component.createNamespace,
})
if err != nil {
errMsg := fmt.Sprintf("error reconciling HelmRelease %s/%s: %s", r.SystemNamespace, component.Template, err)
updateComponentsStatus(detectedComponents, &detectedProviders, detectedContracts, component.helmReleaseName, component.Template, template.Status.Providers, template.Status.CAPIContracts, errMsg)
}); err != nil {
errMsg := fmt.Sprintf("Failed to reconcile HelmRelease %s/%s: %s", r.SystemNamespace, component.helmReleaseName, err)
updateComponentsStatus(statusAccumulator, component, nil, errMsg)
errs = errors.Join(errs, errors.New(errMsg))

continue
}

if component.Template != hmc.CoreHMCName {
if err := r.checkProviderStatus(ctx, component.Template); err != nil {
updateComponentsStatus(detectedComponents, &detectedProviders, detectedContracts, component.helmReleaseName, component.Template, template.Status.Providers, template.Status.CAPIContracts, err.Error())
updateComponentsStatus(statusAccumulator, component, nil, fmt.Sprintf("Failed to check provider status: %s", err))
errs = errors.Join(errs, err)
continue
}
}

updateComponentsStatus(detectedComponents, &detectedProviders, detectedContracts, component.helmReleaseName, component.Template, template.Status.Providers, template.Status.CAPIContracts, "")
updateComponentsStatus(statusAccumulator, component, template, "")
}

management.Status.AvailableProviders = statusAccumulator.providers
management.Status.CAPIContracts = statusAccumulator.compatibilityContracts
management.Status.Components = statusAccumulator.components
management.Status.ObservedGeneration = management.Generation
management.Status.AvailableProviders = detectedProviders
management.Status.CAPIContracts = detectedContracts
management.Status.Components = detectedComponents
management.Status.Release = management.Spec.Release

if err := r.Status().Update(ctx, management); err != nil {
errs = errors.Join(errs, fmt.Errorf("failed to update status for Management %s: %w",
management.Name, err))
errs = errors.Join(errs, fmt.Errorf("failed to update status for Management %s: %w", management.Name, err))
}

if errs != nil {
l.Error(errs, "Multiple errors during Management reconciliation")
return ctrl.Result{}, errs
}

return ctrl.Result{}, nil
}

func (r *ManagementReconciler) cleanupRemovedComponents(ctx context.Context, management *hmc.Management) error {
var (
errs error
l = ctrl.LoggerFrom(ctx)
)

managedHelmReleases := new(fluxv2.HelmReleaseList)
if err := r.Client.List(ctx, managedHelmReleases,
client.MatchingLabels{hmc.HMCManagedLabelKey: hmc.HMCManagedLabelValue},
client.InNamespace(r.SystemNamespace), // all helmreleases are being installed only in the system namespace
); err != nil {
return fmt.Errorf("failed to list %s: %w", fluxv2.GroupVersion.WithKind(fluxv2.HelmReleaseKind), err)
}

for _, hr := range managedHelmReleases.Items {
componentName := hr.Name // providers(components) names map 1-1 to the helmreleases names

if componentName == hmc.CoreCAPIName ||
componentName == hmc.CoreHMCName ||
slices.ContainsFunc(management.Spec.Providers, func(newComp hmc.Provider) bool { return componentName == newComp.Name }) {
continue
}

l.Info("Found component to remove", "component_name", componentName)

if err := r.Client.Delete(ctx, &hr); client.IgnoreNotFound(err) != nil {
errs = errors.Join(errs, fmt.Errorf("failed to delete %s: %w", client.ObjectKeyFromObject(&hr), err))
continue
}
l.Info("Removed HelmRelease", "reference", client.ObjectKeyFromObject(&hr).String())
}

return errs
}

func (r *ManagementReconciler) ensureTemplateManagement(ctx context.Context, mgmt *hmc.Management) error {
l := ctrl.LoggerFrom(ctx)
if !r.CreateTemplateManagement {
Expand Down Expand Up @@ -225,7 +254,7 @@ func (r *ManagementReconciler) ensureTemplateManagement(ctx context.Context, mgm
}

// checkProviderStatus checks the status of a provider associated with a given
// ProviderTemplate name. Since there's no way to determine resource Kind from
// ProviderTemplate name. Since there's no way to determine resource Kind from
// the given template iterate over all possible provider types.
func (r *ManagementReconciler) checkProviderStatus(ctx context.Context, providerTemplateName string) error {
var errs error
Expand Down Expand Up @@ -361,6 +390,16 @@ func applyHMCDefaults(config *apiextensionsv1.JSON) (*apiextensionsv1.JSON, erro
return nil, err
}
}

// Those are only needed for the initial installation
enforcedValues := map[string]any{
"controller": map[string]any{
"createManagement": false,
"createTemplateManagement": false,
"createRelease": false,
},
}

chartutil.CoalesceTables(values, enforcedValues)
raw, err := json.Marshal(values)
if err != nil {
Expand All @@ -369,10 +408,16 @@ func applyHMCDefaults(config *apiextensionsv1.JSON) (*apiextensionsv1.JSON, erro
return &apiextensionsv1.JSON{Raw: raw}, nil
}

func wrappedComponents(mgmt *hmc.Management, release *hmc.Release) ([]component, error) {
func getWrappedComponents(ctx context.Context, cl client.Client, mgmt *hmc.Management) ([]component, error) {
if mgmt.Spec.Core == nil {
return nil, nil
}

release := &hmc.Release{}
if err := cl.Get(ctx, client.ObjectKey{Name: mgmt.Spec.Release}, release); err != nil {
return nil, fmt.Errorf("failed to get Release %s: %w", mgmt.Spec.Release, err)
}

components := make([]component, 0, len(mgmt.Spec.Providers)+2)
hmcComp := component{Component: mgmt.Spec.Core.HMC, helmReleaseName: hmc.CoreHMCName}
if hmcComp.Template == "" {
Expand Down Expand Up @@ -424,8 +469,7 @@ func (r *ManagementReconciler) enableAdditionalComponents(ctx context.Context, m
config := make(map[string]any)

if hmcComponent.Config != nil {
err := json.Unmarshal(hmcComponent.Config.Raw, &config)
if err != nil {
if err := json.Unmarshal(hmcComponent.Config.Raw, &config); err != nil {
return fmt.Errorf("failed to unmarshal HMC config into map[string]any: %w", err)
}
}
Expand All @@ -450,13 +494,15 @@ func (r *ManagementReconciler) enableAdditionalComponents(ctx context.Context, m
capiOperatorValues = v
}

err := certmanager.VerifyAPI(ctx, r.Config, r.SystemNamespace)
if err != nil {
return fmt.Errorf("failed to check in the cert-manager API is installed: %w", err)
if r.Config != nil {
if err := certmanager.VerifyAPI(ctx, r.Config, r.SystemNamespace); err != nil {
return fmt.Errorf("failed to check in the cert-manager API is installed: %w", err)
}

l.Info("Cert manager is installed, enabling the HMC admission webhook")
admissionWebhookValues["enabled"] = true
}
l.Info("Cert manager is installed, enabling the HMC admission webhook")

admissionWebhookValues["enabled"] = true
config["admissionWebhook"] = admissionWebhookValues

// Enable HMC capi operator only if it was not explicitly disabled in the config to
Expand All @@ -475,35 +521,41 @@ func (r *ManagementReconciler) enableAdditionalComponents(ctx context.Context, m
if err != nil {
return fmt.Errorf("failed to marshal HMC config: %w", err)
}
hmcComponent.Config = &apiextensionsv1.JSON{
Raw: updatedConfig,
}

hmcComponent.Config = &apiextensionsv1.JSON{Raw: updatedConfig}

return nil
}

type mgmtStatusAccumulator struct {
components map[string]hmc.ComponentStatus
compatibilityContracts map[string]hmc.CompatibilityContracts
providers hmc.Providers
}

func updateComponentsStatus(
components map[string]hmc.ComponentStatus,
providers *hmc.Providers,
capiContracts map[string]hmc.CompatibilityContracts,
componentName string,
templateName string,
templateProviders hmc.Providers,
templateContracts hmc.CompatibilityContracts,
stAcc *mgmtStatusAccumulator,
comp component,
template *hmc.ProviderTemplate,
err string,
) {
components[componentName] = hmc.ComponentStatus{
if stAcc == nil {
return
}

stAcc.components[comp.helmReleaseName] = hmc.ComponentStatus{
Error: err,
Success: err == "",
Template: templateName,
Template: comp.Component.Template,
}

if err == "" {
*providers = append(*providers, templateProviders...)
slices.Sort(*providers)
*providers = slices.Compact(*providers)
if err == "" && template != nil {
stAcc.providers = append(stAcc.providers, template.Status.Providers...)
slices.Sort(stAcc.providers)
stAcc.providers = slices.Compact(stAcc.providers)

for _, v := range templateProviders {
capiContracts[v] = templateContracts
for _, v := range template.Status.Providers {
stAcc.compatibilityContracts[v] = template.Status.CAPIContracts
}
}
}
Expand Down
Loading

0 comments on commit 2af64cc

Please sign in to comment.