diff --git a/api/v1/resourcegroup_types.go b/api/v1/resourcegroup_types.go index 27ac9fe..a848681 100644 --- a/api/v1/resourcegroup_types.go +++ b/api/v1/resourcegroup_types.go @@ -92,6 +92,11 @@ type ResourceGroupStatus struct { // last applied on the cluster. // +optional Inventory *ResourceInventory `json:"inventory,omitempty"` + + // LastAppliedRevision is the digest of the + // generated resources that were last reconcile. + // +optional + LastAppliedRevision string `json:"lastAppliedRevision,omitempty"` } // GetConditions returns the status conditions of the object. diff --git a/config/crd/bases/fluxcd.controlplane.io_resourcegroups.yaml b/config/crd/bases/fluxcd.controlplane.io_resourcegroups.yaml index d38bb97..34b4f93 100644 --- a/config/crd/bases/fluxcd.controlplane.io_resourcegroups.yaml +++ b/config/crd/bases/fluxcd.controlplane.io_resourcegroups.yaml @@ -217,6 +217,11 @@ spec: required: - entries type: object + lastAppliedRevision: + description: |- + LastAppliedRevision is the digest of the + generated resources that were last reconcile. + type: string lastHandledReconcileAt: description: |- LastHandledReconcileAt holds the value of the most recent diff --git a/internal/controller/resourcegroup_controller.go b/internal/controller/resourcegroup_controller.go index bc3e876..b489459 100644 --- a/internal/controller/resourcegroup_controller.go +++ b/internal/controller/resourcegroup_controller.go @@ -22,6 +22,7 @@ import ( "github.com/google/cel-go/cel" "github.com/google/cel-go/common/types" "github.com/google/cel-go/ext" + "github.com/opencontainers/go-digest" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -157,7 +158,8 @@ func (r *ResourceGroupReconciler) reconcile(ctx context.Context, } // Apply the resources to the cluster. - if err := r.apply(ctx, obj, buildResult); err != nil { + applySetDigest, err := r.apply(ctx, obj, buildResult) + if err != nil { msg := fmt.Sprintf("reconciliation failed: %s", err.Error()) conditions.MarkFalse(obj, meta.ReadyCondition, @@ -168,7 +170,8 @@ func (r *ResourceGroupReconciler) reconcile(ctx context.Context, return ctrl.Result{}, err } - // Mark the object as ready. + // Mark the object as ready and set the last applied revision. + obj.Status.LastAppliedRevision = applySetDigest msg = fmt.Sprintf("Reconciliation finished in %s", fmtDuration(reconcileStart)) conditions.MarkTrue(obj, meta.ReadyCondition, @@ -262,9 +265,11 @@ func (r *ResourceGroupReconciler) checkDependencies(ctx context.Context, // apply reconciles the resources in the cluster by performing // a server-side apply, pruning of stale resources and waiting // for the resources to become ready. +// It returns an error if the apply operation fails, otherwise +// it returns the sha256 digest of the applied resources. func (r *ResourceGroupReconciler) apply(ctx context.Context, obj *fluxcdv1.ResourceGroup, - objects []*unstructured.Unstructured) error { + objects []*unstructured.Unstructured) (string, error) { log := ctrl.LoggerFrom(ctx) var changeSetLog strings.Builder @@ -289,7 +294,7 @@ func (r *ResourceGroupReconciler) apply(ctx context.Context, // Create the Kubernetes client that runs under impersonation. kubeClient, statusPoller, err := impersonation.GetClient(ctx) if err != nil { - return fmt.Errorf("failed to build kube client: %w", err) + return "", fmt.Errorf("failed to build kube client: %w", err) } // Create a resource manager to reconcile the resources. @@ -300,13 +305,20 @@ func (r *ResourceGroupReconciler) apply(ctx context.Context, resourceManager.SetOwnerLabels(objects, obj.GetName(), obj.GetNamespace()) if err := normalize.UnstructuredList(objects); err != nil { - return err + return "", err } if cm := obj.Spec.CommonMetadata; cm != nil { ssautil.SetCommonMetadata(objects, cm.Labels, cm.Annotations) } + // Compute the sha256 digest of the resources. + data, err := ssautil.ObjectsToYAML(objects) + if err != nil { + return "", fmt.Errorf("failed to convert objects to YAML: %w", err) + } + applySetDigest := digest.FromString(data).String() + applyOpts := ssa.DefaultApplyOptions() applyOpts.Cleanup = ssa.ApplyCleanupOptions{ // Remove the kubectl and helm annotations. @@ -347,7 +359,7 @@ func (r *ResourceGroupReconciler) apply(ctx context.Context, // Apply the resources to the cluster. changeSet, err := resourceManager.ApplyAllStaged(ctx, objects, applyOpts) if err != nil { - return err + return "", err } // Filter out the resources that have changed. @@ -368,7 +380,7 @@ func (r *ResourceGroupReconciler) apply(ctx context.Context, newInventory := inventory.New() err = inventory.AddChangeSet(newInventory, changeSet) if err != nil { - return err + return "", err } // Set last applied inventory in status. @@ -377,7 +389,7 @@ func (r *ResourceGroupReconciler) apply(ctx context.Context, // Detect stale resources which are subject to garbage collection. staleObjects, err := inventory.Diff(oldInventory, newInventory) if err != nil { - return err + return "", err } // Garbage collect stale resources. @@ -392,7 +404,7 @@ func (r *ResourceGroupReconciler) apply(ctx context.Context, deleteSet, err := r.deleteAllStaged(ctx, resourceManager, staleObjects, deleteOpts) if err != nil { - return err + return "", err } if len(deleteSet.Entries) > 0 { @@ -421,12 +433,12 @@ func (r *ResourceGroupReconciler) apply(ctx context.Context, FailFast: true, }); err != nil { readyStatus := r.aggregateNotReadyStatus(ctx, kubeClient, objects) - return fmt.Errorf("%w\n%s", err, readyStatus) + return "", fmt.Errorf("%w\n%s", err, readyStatus) } log.Info("Health check completed") } - return nil + return applySetDigest, nil } // aggregateNotReadyStatus returns the status of the Flux resources not ready. diff --git a/internal/controller/resourcegroup_controller_test.go b/internal/controller/resourcegroup_controller_test.go index d4ebdb4..8adc804 100644 --- a/internal/controller/resourcegroup_controller_test.go +++ b/internal/controller/resourcegroup_controller_test.go @@ -110,6 +110,10 @@ spec: }, )) + // Check if the status last applied revision was set. + g.Expect(result.Status.LastAppliedRevision).ToNot(BeEmpty()) + lastAppliedRevision := result.Status.LastAppliedRevision + // Check if the resources were created and labeled. resultSA := &corev1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ @@ -177,6 +181,10 @@ spec: }, )) + // Check if the status last applied revision was updated. + g.Expect(resultFinal.Status.LastAppliedRevision).ToNot(BeEmpty()) + g.Expect(resultFinal.Status.LastAppliedRevision).ToNot(BeEquivalentTo(lastAppliedRevision)) + // Check if the resources were deleted. err = testClient.Get(ctx, client.ObjectKeyFromObject(resultSA), resultSA) g.Expect(err).To(HaveOccurred())