diff --git a/api/v1alpha1/appgroup_types.go b/api/v1alpha1/appgroup_types.go index e3b867e..d09d8b5 100644 --- a/api/v1alpha1/appgroup_types.go +++ b/api/v1alpha1/appgroup_types.go @@ -10,12 +10,13 @@ import ( // ApplicationSpec defines the desired state of Application type ApplicationSpec struct { - // Namespace to which the HelmRelease object will be deployed - Namespace string `json:"namespace,omitempty"` + // Subcharts provides the dependency order among the subcharts of the application Subcharts []DAG `json:"subcharts,omitempty"` GroupID string `json:"groupID,omitempty"` + // ChartRepoNickname is used to lookup the repository config in the registries config map ChartRepoNickname string `json:"repo,omitempty"` + // XXX (nitishm) **IMPORTANT**: DO NOT USE HelmReleaseSpec.Values!!! // ApplicationSpec.Overlays field replaces HelmReleaseSpec.Values field. // Setting the HelmReleaseSpec.Values field will not reflect in the deployed Application object @@ -36,37 +37,56 @@ type ApplicationSpec struct { // Artifactory for instance utilizes folders to store charts RepoPath string `json:"repoPath,omitempty"` + // Inline HelmReleaseSpec from the flux helm-operator package helmopv1.HelmReleaseSpec `json:",inline"` } -// ChartStatus denotes the current status of the Application Reconciliation +// ChartStatus shows the current status of the Application Reconciliation process type ChartStatus struct { - Phase helmopv1.HelmReleasePhase `json:"phase,omitempty"` - Error string `json:"error,omitempty"` - Version string `json:"version,omitempty"` - Staged bool `json:"staged,omitempty"` + // Phase reflects the current state of the HelmRelease + Phase helmopv1.HelmReleasePhase `json:"phase,omitempty"` + // Error string from the error during reconciliation (if any) + Error string `json:"error,omitempty"` + // Version of the chart/subchart + Version string `json:"version,omitempty"` + // Staged if true denotes that the chart/subchart has been pushed to the + // staging helm repo + Staged bool `json:"staged,omitempty"` } // ApplicationGroupSpec defines the desired state of ApplicationGroup type ApplicationGroupSpec struct { + // Applications that make up the application group Applications []Application `json:"applications,omitempty"` } +// Application spec and dependency on other applications type Application struct { - DAG `json:",inline"` + // DAG contains the dependency information + DAG `json:",inline"` + // Spec contains the application spec including the chart info and overlay values Spec ApplicationSpec `json:"spec,omitempty"` } + +// DAG contains the dependency information type DAG struct { - Name string `json:"name,omitempty"` + // Name of the application + Name string `json:"name,omitempty"` + // Dependencies on other applications by name Dependencies []string `json:"dependencies,omitempty"` } +// ApplicationStatus shows the current status of the application helm release type ApplicationStatus struct { - Name string `json:"name"` + // Name of the application + Name string `json:"name"` + // ChartStatus for the application helm chart ChartStatus `json:",inline"` - Subcharts map[string]ChartStatus `json:"subcharts,omitempty"` + // Subcharts contains the subchart chart status + Subcharts map[string]ChartStatus `json:"subcharts,omitempty"` } +// ReconciliationPhase is an enum type ReconciliationPhase string const ( @@ -79,11 +99,17 @@ const ( // ApplicationGroupStatus defines the observed state of ApplicationGroup type ApplicationGroupStatus struct { - Checksums map[string]string `json:"checksums,omitempty"` + // Checksums for each application are calculated from the application spec + // The status/metadata information is ignored + Checksums map[string]string `json:"checksums,omitempty"` + // Applications status Applications []ApplicationStatus `json:"status,omitempty"` - Phase ReconciliationPhase `json:"phase,omitempty"` - Update bool `json:"update,omitempty"` - Error string `json:"error,omitempty"` + // Phase is the reconciliation phase + Phase ReconciliationPhase `json:"phase,omitempty"` + // Update is an internal flag used to trigger a workflow update + Update bool `json:"update,omitempty"` + // Error string from errors during reconciliation + Error string `json:"error,omitempty"` } // +kubebuilder:object:root=true diff --git a/controllers/appgroup_controller.go b/controllers/appgroup_controller.go index 151cbaa..e3e45be 100644 --- a/controllers/appgroup_controller.go +++ b/controllers/appgroup_controller.go @@ -66,6 +66,9 @@ type ApplicationGroupReconciler struct { // Recorder generates kubernetes events Recorder record.EventRecorder + // lastSuccessfulApplicationGroup holds the applicationgroup spec body from the last + // successful reconciliation of the ApplicationGroup. This is set after every successful + // reconciliation. lastSuccessfulApplicationGroup *orkestrav1alpha1.ApplicationGroup } @@ -89,6 +92,9 @@ func (r *ApplicationGroupReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e return ctrl.Result{}, err } + // Check if this is an update event to the ApplicationGroup + // in which case unmarshal the last successful spec into a + // variable if appGroup.GetAnnotations() != nil { last := &orkestrav1alpha1.ApplicationGroup{} if s, ok := appGroup.Annotations[lastSuccessfulApplicationGroupKey]; ok { @@ -97,13 +103,17 @@ func (r *ApplicationGroupReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e } } - // handle deletes if deletion timestamp is non-zero + // handle deletes if deletion timestamp is non-zero. + // controller-runtime cannot guarantee the order of events + // , so it is upto us to determine the type of event if !appGroup.DeletionTimestamp.IsZero() { // If finalizer is found, remove it and requeue if appGroup.Finalizers != nil { logr.Info("cleaning up the applicationgroup resource") // Reverse the entire workflow to remove all the Helm Releases + + // unset the last successful spec annotation r.lastSuccessfulApplicationGroup = nil if _, ok := appGroup.Annotations[lastSuccessfulApplicationGroupKey]; ok { appGroup.Annotations[lastSuccessfulApplicationGroupKey] = "" @@ -128,6 +138,9 @@ func (r *ApplicationGroupReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e return ctrl.Result{Requeue: true}, nil } + // If the (needs) Rollback phase is present in the reconciled version, + // we must rollback the application group to the last successful spec. + // This should only happen on updates and not during installs. if appGroup.Status.Phase == orkestrav1alpha1.Rollback { logr.Info("Rolling back to last successful application group spec") appGroup.Spec = r.lastSuccessfulApplicationGroup.DeepCopy().Spec @@ -145,8 +158,10 @@ func (r *ApplicationGroupReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e // handle first time install and subsequent updates checksums, err := pkg.Checksum(&appGroup) if err != nil { + // determine if the spec has changed if errors.Is(err, pkg.ErrChecksumAppGroupSpecMismatch) { if appGroup.Status.Checksums != nil { + // flag this as requiring workflow updates for the reconciler appGroup.Status.Update = true } requeue, err = r.reconcile(ctx, logr, r.WorkflowNS, &appGroup) @@ -184,7 +199,13 @@ func (r *ApplicationGroupReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e appGroup.Status.Checksums = checksums + // Calculate the cumulative status of the generated Workflow + // and the generated HelmRelease objects + // Lookup Workflow by ownership and heritage labels + // We are expecting to find at most one workflow in + // the returned list that is associated with this + // ApplicationGroup object. wfs := v1alpha12.WorkflowList{} listOption := client.MatchingLabels{ workflow.OwnershipLabel: appGroup.Name, @@ -198,13 +219,13 @@ func (r *ApplicationGroupReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e } if wfs.Items.Len() == 0 { - // appGroup.Status.Phase = wfs.Items[0].Status.Phase err = fmt.Errorf("listed workflows len is 0") logr.Error(err, "no associated workflow found") requeue = false return r.handleResponseAndEvent(ctx, logr, appGroup, requeue, err) } + // determine the associated/generated workflow status var phase orkestrav1alpha1.ReconciliationPhase wfStatus := wfs.Items[0].Status.Phase switch wfStatus { @@ -249,6 +270,7 @@ func (r *ApplicationGroupReconciler) Reconcile(req ctrl.Request) (ctrl.Result, e } } + // Update each application status using the HelmRelease status v := make([]orkestrav1alpha1.ApplicationStatus, 0) for _, app := range appGroup.Status.Applications { app.ChartStatus.Phase = helmReleaseStatusMap[app.Name] @@ -333,28 +355,6 @@ func (r *ApplicationGroupReconciler) handleResponseAndEvent(ctx context.Context, return reconcile.Result{Requeue: requeue}, err } -// 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 { @@ -382,8 +382,11 @@ func initApplications(appGroup *orkestrav1alpha1.ApplicationGroup) { } func (r *ApplicationGroupReconciler) handleRemediation(ctx context.Context, logr logr.Logger, g orkestrav1alpha1.ApplicationGroup, err error) (ctrl.Result, error) { - // Rollback to previous successful spec + // Rollback to previous successful spec since the annotation was set and this is + // an UPDATE event if r.lastSuccessfulApplicationGroup != nil { + // If this is a HelmRelease failure then we must remediate by cleaning up + // all the helm releases deployed by the workflow and helm operator if errors.Is(err, ErrHelmReleaseInFailureStatus) { // Delete the HelmRelease(s) - parent and subchart(s) // Lookup charts using the label selector. @@ -412,6 +415,9 @@ func (r *ApplicationGroupReconciler) handleRemediation(ctx context.Context, logr } } } + // mark the object as requiring rollback so that we can rollback + // to the previous versions of all the applications in the ApplicationGroup + // using the last successful spec g.Status.Phase = orkestrav1alpha1.Rollback _ = r.Status().Update(ctx, &g) diff --git a/pkg/registry/pull.go b/pkg/registry/pull.go index 4b3a1fa..25b60a3 100644 --- a/pkg/registry/pull.go +++ b/pkg/registry/pull.go @@ -10,6 +10,7 @@ import ( ) func (c *Client) PullChart(l logr.Logger, repoKey, repoPath, chartName, version string) (string, *chart.Chart, error) { + // logic is derived from the "helm pull" command from the helm cli package l.WithValues("repo-key", repoKey, "chart-name", chartName, "chart-version", version) l.V(3).Info("pulling chart") diff --git a/pkg/registry/push.go b/pkg/registry/push.go index dfd962b..11a5ec7 100644 --- a/pkg/registry/push.go +++ b/pkg/registry/push.go @@ -13,6 +13,7 @@ import ( // PushChart pushes the chart to the repository specified by the repoKey. The repository setting is fetched from the associated registry config file func (c *Client) PushChart(l logr.Logger, repoKey, pkgPath string, ch *chart.Chart) error { + // logic is derived from the "helm push" extension from the chartmuseum folks chartName := ch.Name() version := ch.Metadata.Version