Skip to content

Commit

Permalink
Add main management-controller logic
Browse files Browse the repository at this point in the history
HMC-23
  • Loading branch information
eromanova committed Jun 14, 2024
1 parent 27f0c68 commit 2321134
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 29 deletions.
31 changes: 15 additions & 16 deletions internal/controller/deployment_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"fmt"

hcv2 "github.com/fluxcd/helm-controller/api/v2"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
Expand Down Expand Up @@ -87,7 +86,7 @@ func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request)
l.Info("Applying default configuration")
return ctrl.Result{}, r.Client.Update(ctx, deployment)
}
source, err := r.getSource(ctx, template.Status.ChartRef)
source, err := helm.GetSource(ctx, r.Client, template.Status.ChartRef)
if err != nil {
_ = r.updateStatus(ctx, deployment, fmt.Sprintf("failed to get helm chart source: %s", err))
return ctrl.Result{}, err
Expand All @@ -109,12 +108,24 @@ func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request)
}

l.Info("Validating Helm chart with provided values")
if err := r.validateReleaseWithValues(ctx, actionConfig, deployment, hcChart); err != nil {
values, err := deployment.HelmValues()
if err != nil {
_ = r.updateStatus(ctx, deployment, fmt.Sprintf("failed to get helm values: %s", err))
return ctrl.Result{}, err
}
if err := helm.ValidateReleaseWithValues(ctx, actionConfig, deployment.Name, deployment.Namespace, values, hcChart); err != nil {
_ = r.updateStatus(ctx, deployment, fmt.Sprintf("failed to validate template with provided configuration: %s", err))
return ctrl.Result{}, err
}
if !deployment.Spec.DryRun {
_, err := r.reconcileHelmRelease(ctx, deployment, template.Status.ChartRef)
ownerRef := metav1.OwnerReference{
APIVersion: hmc.GroupVersion.String(),
Kind: hmc.DeploymentKind,
Name: deployment.Name,
UID: deployment.UID,
}
_, err := helm.ReconcileHelmRelease(ctx, r.Client, deployment.Name, deployment.Namespace, deployment.Spec.Config,
ownerRef, template.Status.ChartRef, defaultReconcileInterval)
if err != nil {
return ctrl.Result{}, err
}
Expand Down Expand Up @@ -181,18 +192,6 @@ func (r *DeploymentReconciler) updateStatus(ctx context.Context, deployment *hmc
return nil
}

func (r *DeploymentReconciler) getSource(ctx context.Context, ref *hcv2.CrossNamespaceSourceReference) (sourcev1.Source, error) {
if ref == nil {
return nil, fmt.Errorf("helm chart source is not provided")
}
chartRef := types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}
hc := sourcev1.HelmChart{}
if err := r.Client.Get(ctx, chartRef, &hc); err != nil {
return nil, err
}
return &hc, nil
}

func applyDefaultConfiguration(deployment *hmc.Deployment, template *hmc.Template) (changed bool) {
if deployment.Spec.Config != nil || template.Status.Config == nil {
// Only apply defaults when there's no configuration provided
Expand Down
134 changes: 121 additions & 13 deletions internal/controller/management_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,45 +18,153 @@ package controller

import (
"context"
"fmt"

"github.com/Mirantis/hmc/internal/helm"
"helm.sh/helm/v3/pkg/action"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"

hmcmirantiscomv1alpha1 "github.com/Mirantis/hmc/api/v1alpha1"
hmc "github.com/Mirantis/hmc/api/v1alpha1"
)

// ManagementReconciler reconciles a Management object
type ManagementReconciler struct {
client.Client
Scheme *runtime.Scheme
Config *rest.Config
}

//+kubebuilder:rbac:groups=hmc.mirantis.com,resources=managements,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=hmc.mirantis.com,resources=managements/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=hmc.mirantis.com,resources=managements/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Management object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
func (r *ManagementReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)
l := log.FromContext(ctx).WithValues("ManagementController", req.NamespacedName)
l.Info("Reconciling Management")

// TODO(user): your logic here
management := &hmc.Management{}
if err := r.Get(ctx, req.NamespacedName, management); err != nil {
if errors.IsNotFound(err) {
l.Info("Management config not found, creating default configuration")
err = r.ensureDefaultManagement(ctx)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
l.Error(err, "Failed to get Management")
return ctrl.Result{}, err
}

for _, component := range management.Spec.Components {
template := &hmc.Template{}
err := r.Get(ctx, types.NamespacedName{
Namespace: hmc.TemplatesNamespace,
Name: component.Template,
}, template)
if err != nil {
l.Error(err, "Failed to get Template", "namespace", hmc.TemplatesNamespace, "name", component.Template)
continue
}
if !template.Status.Valid {
errMsg := fmt.Sprintf("Template %s/%s is not marked as valid", hmc.TemplatesNamespace, component.Template)
_ = r.updateStatus(ctx, management, component.Template, errMsg)
return ctrl.Result{}, fmt.Errorf(errMsg)
}

// Applying defaults
if component.Config == nil && template.Status.Config != nil {
component.Config = &apiextensionsv1.JSON{Raw: template.Status.Config.Raw}
}

source, err := helm.GetSource(ctx, r.Client, template.Status.ChartRef)
if err != nil {
_ = r.updateStatus(ctx, management, component.Template, fmt.Sprintf("failed to get helm chart source: %s", err))
return ctrl.Result{}, err
}
l.Info("Downloading Helm chart")
hcChart, err := helm.DownloadChartFromArtifact(ctx, source.GetArtifact())
if err != nil {
_ = r.updateStatus(ctx, management, component.Template, fmt.Sprintf("failed to download helm chart: %s", err))
return ctrl.Result{}, err
}

l.Info("Initializing Helm client")
getter := helm.NewMemoryRESTClientGetter(r.Config, r.RESTMapper())
actionConfig := new(action.Configuration)
err = actionConfig.Init(getter, management.Namespace, "secret", l.Info)
if err != nil {
_ = r.updateStatus(ctx, management, component.Template, fmt.Sprintf("failed to initialize helm client: %s", err))
return ctrl.Result{}, err
}

l.Info("Validating Helm chart with provided values")
values, err := component.HelmValues()
if err != nil {
_ = r.updateStatus(ctx, management, component.Template, fmt.Sprintf("failed to get helm values: %s", err))
return ctrl.Result{}, err
}
if err := helm.ValidateReleaseWithValues(ctx, actionConfig, management.Name, management.Namespace, values, hcChart); err != nil {
_ = r.updateStatus(ctx, management, component.Template, fmt.Sprintf("failed to validate template with provided configuration: %s", err))
return ctrl.Result{}, err
}

ownerRef := metav1.OwnerReference{
APIVersion: hmc.GroupVersion.String(),
Kind: hmc.DeploymentKind,
Name: management.Name,
UID: management.UID,
}
_, err = helm.ReconcileHelmRelease(ctx, r.Client, management.Name, management.Namespace, component.Config,
ownerRef, template.Status.ChartRef, defaultReconcileInterval)
if err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}

func (r *ManagementReconciler) ensureDefaultManagement(ctx context.Context) error {
management := &hmc.Management{}
management.Name = hmc.ManagementName
management.Namespace = hmc.ManagementNamespace
management.Spec.SetDefaults()
err := r.Create(ctx, management)
if err != nil {
return err
}
return nil
}

func (r *ManagementReconciler) updateStatus(ctx context.Context, management *hmc.Management, componentName, err string) error {
management.Status.ObservedGeneration = management.Generation
components := management.Status.Components
if components == nil {
components = make(map[string]hmc.ComponentStatus)
}
components[componentName] = hmc.ComponentStatus{
Error: err,
Success: err == "",
}
management.Status.Components = components

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

// SetupWithManager sets up the controller with the Manager.
func (r *ManagementReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&hmcmirantiscomv1alpha1.Management{}).
For(&hmc.Management{}).
Complete(r)
}
45 changes: 45 additions & 0 deletions internal/helm/release.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package helm

import (
"context"
"time"

hcv2 "github.com/fluxcd/helm-controller/api/v2"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func ReconcileHelmRelease(
ctx context.Context,
cl client.Client,
name string,
namespace string,
values *apiextensionsv1.JSON,
ownerReference metav1.OwnerReference,
chartRef *hcv2.CrossNamespaceSourceReference,
reconcileInterval time.Duration,
) (*hcv2.HelmRelease, error) {
helmRelease := &hcv2.HelmRelease{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
}

_, err := ctrl.CreateOrUpdate(ctx, cl, helmRelease, func() error {
helmRelease.OwnerReferences = []metav1.OwnerReference{ownerReference}
helmRelease.Spec = hcv2.HelmReleaseSpec{
ChartRef: chartRef,
Interval: metav1.Duration{Duration: reconcileInterval},
ReleaseName: name,
Values: values,
}
return nil
})
if err != nil {
return nil, err
}
return helmRelease, nil
}
15 changes: 15 additions & 0 deletions internal/helm/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,29 @@ import (
"io"
"net/http"

hcv2 "github.com/fluxcd/helm-controller/api/v2"
sourcev1 "github.com/fluxcd/source-controller/api/v1"
"github.com/hashicorp/go-retryablehttp"
godigest "github.com/opencontainers/go-digest"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)

func GetSource(ctx context.Context, cl client.Client, ref *hcv2.CrossNamespaceSourceReference) (sourcev1.Source, error) {
if ref == nil {
return nil, fmt.Errorf("helm chart source is not provided")
}
chartRef := types.NamespacedName{Namespace: ref.Namespace, Name: ref.Name}
hc := sourcev1.HelmChart{}
if err := cl.Get(ctx, chartRef, &hc); err != nil {
return nil, err
}
return &hc, nil
}

func DownloadChartFromArtifact(ctx context.Context, artifact *sourcev1.Artifact) (*chart.Chart, error) {
return DownloadChart(ctx, artifact.URL, artifact.Digest)
}
Expand Down
29 changes: 29 additions & 0 deletions internal/helm/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package helm

import (
"context"

"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
)

func ValidateReleaseWithValues(
ctx context.Context,
actionConfig *action.Configuration,
releaseNamespace string,
releaseName string,
values map[string]interface{},
hcChart *chart.Chart,
) error {
install := action.NewInstall(actionConfig)
install.DryRun = true
install.ReleaseName = releaseName
install.Namespace = releaseNamespace
install.ClientOnly = false

_, err := install.RunWithContext(ctx, hcChart, values)
if err != nil {
return err
}
return nil
}

0 comments on commit 2321134

Please sign in to comment.