orkestra/controllers/appgroup_controller.go

252 строки
8.2 KiB
Go

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package controllers
import (
"context"
"errors"
"fmt"
"net/url"
"strings"
"time"
"github.com/Azure/Orkestra/pkg"
"github.com/Azure/Orkestra/pkg/configurer"
"github.com/Azure/Orkestra/pkg/registry"
"github.com/Azure/Orkestra/pkg/workflow"
v1alpha12 "github.com/argoproj/argo/pkg/apis/workflow/v1alpha1"
"github.com/go-logr/logr"
"helm.sh/helm/v3/pkg/chart"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/predicate"
orkestrav1alpha1 "github.com/Azure/Orkestra/api/v1alpha1"
)
const (
appgroupNameKey = "appgroup"
finalizer = "application-group-finalizer"
requeueAfter = 5 * time.Second
)
// ApplicationGroupReconciler reconciles a ApplicationGroup object
type ApplicationGroupReconciler struct {
client.Client
Log logr.Logger
Scheme *runtime.Scheme
// Cfg is the controller configuration that gives access to the helm registry configuration (and more as we add options to configure the controller)
Cfg *configurer.Controller
Engine workflow.Engine
// RegistryClient interacts with the helm registries to pull and push charts
RegistryClient *registry.Client
// WorkflowNS is the namespace to which (generated) Argo Workflow object is deployed
WorkflowNS string
// StagingRepoName is the nickname for the repository used for staging artifacts before being deployed using the HelmRelease object
StagingRepoName string
// TargetDir to stage the charts before pushing
TargetDir string
// Recorder generates kubernetes events
Recorder record.EventRecorder
}
// +kubebuilder:rbac:groups=orkestra.azure.microsoft.com,resources=applicationgroups,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=orkestra.azure.microsoft.com,resources=applicationgroups/status,verbs=get;update;patch
func (r *ApplicationGroupReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
var requeue bool
var err error
var appGroup orkestrav1alpha1.ApplicationGroup
ctx := context.Background()
logr := r.Log.WithValues(appgroupNameKey, req.NamespacedName.Name)
if err := r.Get(ctx, req.NamespacedName, &appGroup); err != nil {
if kerrors.IsNotFound(err) {
logr.V(3).Info("skip reconciliation since AppGroup instance not found on the cluster")
return ctrl.Result{}, nil
}
logr.Error(err, "unable to fetch ApplicationGroup instance")
return ctrl.Result{}, err
}
// handle DELETE if deletion timestamp is non-zero
if !appGroup.DeletionTimestamp.IsZero() {
// If finalizer is found, remove it and requeue
if appGroup.Finalizers != nil {
logr.Info("Cleaning up")
// TODO: Take remediation action
// Reverse the entire workflow to remove all the Helm Releases
appGroup.Finalizers = nil
_ = r.Update(ctx, &appGroup)
return ctrl.Result{Requeue: true}, nil
}
// Do nothing
return ctrl.Result{Requeue: false}, nil
}
// Initialize all the application specs and status fields embedded in the application group
initApplications(&appGroup)
// Add finalizer if it doesnt already exist
if appGroup.Finalizers == nil {
appGroup.Finalizers = []string{finalizer}
_ = r.Update(ctx, &appGroup)
return ctrl.Result{Requeue: true}, nil
}
// handle UPDATE if checksum mismatched
checksums, err := pkg.Checksum(&appGroup)
if err != nil {
// TODO (nitishm) Handle different error types here to decide remediation action
if errors.Is(err, pkg.ErrChecksumAppGroupSpecMismatch) {
appGroup.Status.Checksums = checksums
appGroup.Status.Update = true
requeue, err = r.reconcile(ctx, logr, r.WorkflowNS, &appGroup)
defer r.updateStatusAndEvent(ctx, appGroup, requeue, err)
if err != nil {
logr.Error(err, "failed to reconcile ApplicationGroup instance")
return ctrl.Result{Requeue: requeue}, err
}
if appGroup.Status.Phase != v1alpha12.NodeSucceeded {
return ctrl.Result{Requeue: true, RequeueAfter: requeueAfter}, nil
}
return ctrl.Result{Requeue: false}, nil
}
appGroup.Status.Error = err.Error()
_ = r.Status().Update(ctx, &appGroup)
logr.Error(err, "failed to calculate checksum annotations for application group specs")
return ctrl.Result{Requeue: false}, err
}
appGroup.Status.Checksums = checksums
// Lookup Workflow by ownership and heritage labels
wfs := v1alpha12.WorkflowList{}
listOption := client.MatchingLabels{
workflow.OwnershipLabel: appGroup.Name,
workflow.HeritageLabel: workflow.Project,
}
err = r.List(ctx, &wfs, listOption)
if err != nil {
logr.Error(err, "failed to find generate workflow instance")
appGroup.Status.Error = err.Error()
_ = r.Status().Update(ctx, &appGroup)
return ctrl.Result{Requeue: false}, err
}
if wfs.Items.Len() > 0 {
appGroup.Status.Phase = wfs.Items[0].Status.Phase
}
logr = logr.WithValues("phase", appGroup.Status.Phase, "status-error", appGroup.Status.Error)
switch appGroup.Status.Phase {
case v1alpha12.NodeRunning, v1alpha12.NodePending:
logr.V(1).Info("workflow in pending/running state. requeue and reconcile after a short period")
_ = r.Status().Update(ctx, &appGroup)
return ctrl.Result{Requeue: true, RequeueAfter: requeueAfter}, nil
case v1alpha12.NodeSucceeded:
logr.V(1).Info("workflow ran to completion and succeeded")
appGroup.Status.Error = ""
r.updateStatusAndEvent(ctx, appGroup, false, nil)
return ctrl.Result{Requeue: false}, nil
case v1alpha12.NodeError, v1alpha12.NodeFailed:
err = fmt.Errorf("workflow in failure/error condition")
logr.Error(err, "workflow in failure/error condition")
appGroup.Status.Error = err.Error()
_ = r.Status().Update(ctx, &appGroup)
return ctrl.Result{Requeue: false}, err
}
return ctrl.Result{Requeue: false}, nil
}
func (r *ApplicationGroupReconciler) SetupWithManager(mgr ctrl.Manager) error {
pred := predicate.GenerationChangedPredicate{}
return ctrl.NewControllerManagedBy(mgr).
For(&orkestrav1alpha1.ApplicationGroup{}).
WithEventFilter(pred).
Complete(r)
}
func (r *ApplicationGroupReconciler) updateStatusAndEvent(ctx context.Context, grp orkestrav1alpha1.ApplicationGroup, requeue bool, err error) {
errStr := ""
if err != nil {
errStr = err.Error()
}
grp.Status.Error = errStr
_ = r.Status().Update(ctx, &grp)
if grp.Status.Phase == v1alpha12.NodeSucceeded {
r.Recorder.Event(&grp, "Normal", "ReconcileSuccess", fmt.Sprintf("Successfully reconciled ApplicationGroup %s", grp.Name))
}
if errStr != "" {
r.Recorder.Event(&grp, "Warning", "ReconcileError", fmt.Sprintf("Failed to reconcile ApplicationGroup %s with Error %s", grp.Name, errStr))
}
}
func isDependenciesEmbedded(ch *chart.Chart) bool {
// TODO (nitishm) This does not support a mix of remote and embedded dependency subcharts
isURI := false
for _, d := range ch.Metadata.Dependencies {
if _, err := url.ParseRequestURI(d.Repository); err == nil {
// If this is an "assembled" chart (https://helm.sh/docs/chart_best_practices/dependencies/#versions) we must stage the embedded subchart
if strings.Contains(d.Repository, "file://") {
isURI = false
break
}
isURI = true
}
}
if !isURI {
if len(ch.Dependencies()) > 0 {
return true
}
}
return false
}
func initApplications(appGroup *orkestrav1alpha1.ApplicationGroup) {
// Initialize the Status fields if not already setup
if len(appGroup.Status.Applications) == 0 {
appGroup.Status.Applications = make([]orkestrav1alpha1.ApplicationStatus, 0, len(appGroup.Spec.Applications))
for _, app := range appGroup.Spec.Applications {
status := orkestrav1alpha1.ApplicationStatus{
Name: app.Name,
ChartStatus: orkestrav1alpha1.ChartStatus{Version: app.Spec.Version},
Subcharts: make(map[string]orkestrav1alpha1.ChartStatus),
}
appGroup.Status.Applications = append(appGroup.Status.Applications, status)
}
}
// Initialize fields in the Application spec for every app in the appgroup
v := orkestrav1alpha1.ApplicationGroup{}
for _, app := range appGroup.Spec.Applications {
if app.Spec.Overlays.Data == nil {
app.Spec.Overlays.Data = make(map[string]interface{})
}
app.Spec.Values = app.Spec.Overlays
v.Spec.Applications = append(v.Spec.Applications, app)
}
appGroup.Spec.Applications = v.DeepCopy().Spec.Applications
}