From 677a996106a6b7d630babeb61b0242f381eb686d Mon Sep 17 00:00:00 2001 From: Ilya Dmitrichenko Date: Tue, 12 Mar 2024 12:38:57 +0000 Subject: [PATCH 01/10] Factor out `applyInstance` function from `apply_bundle.go` (WIP) Signed-off-by: Ilya Dmitrichenko --- cmd/timoni/bundle_apply.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cmd/timoni/bundle_apply.go b/cmd/timoni/bundle_apply.go index 3891ab85..a7b6da9e 100644 --- a/cmd/timoni/bundle_apply.go +++ b/cmd/timoni/bundle_apply.go @@ -316,18 +316,22 @@ func applyBundleInstance(ctx context.Context, cuectx *cue.Context, instance *eng return describeErr(modDir, "build failed for "+instance.Name, err) } + return applyInstance(ctx, log, builder, buildResult, instance, rootDir) +} + +func applyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleBuilder, buildResult cue.Value, instance *engine.BundleInstance, rootDir string) error { finalValues, err := builder.GetDefaultValues() if err != nil { return fmt.Errorf("failed to extract values: %w", err) } - bundleApplySets, err := builder.GetApplySets(buildResult) + sets, err := builder.GetApplySets(buildResult) if err != nil { return fmt.Errorf("failed to extract objects: %w", err) } var objects []*unstructured.Unstructured - for _, set := range bundleApplySets { + for _, set := range sets { objects = append(objects, set.Objects...) } @@ -411,8 +415,8 @@ func applyBundleInstance(ctx context.Context, cuectx *cue.Context, instance *eng FailFast: true, } - for _, set := range bundleApplySets { - if len(bundleApplySets) > 1 { + for _, set := range sets { + if len(sets) > 1 { log.Info(fmt.Sprintf("applying %s", set.Name)) } From dcbf2f7012181338f7294500d4d621bd2d250821 Mon Sep 17 00:00:00 2001 From: Ilya Dmitrichenko Date: Tue, 12 Mar 2024 14:13:37 +0000 Subject: [PATCH 02/10] Adapt `applyInstance` function to suite `apply.go` (WIP) Signed-off-by: Ilya Dmitrichenko --- cmd/timoni/apply.go | 156 ++----------------------------------- cmd/timoni/bundle_apply.go | 25 ++++-- 2 files changed, 25 insertions(+), 156 deletions(-) diff --git a/cmd/timoni/apply.go b/cmd/timoni/apply.go index 9867d369..6eadbfb6 100644 --- a/cmd/timoni/apply.go +++ b/cmd/timoni/apply.go @@ -22,13 +22,9 @@ import ( "fmt" "os" "strings" - "time" "cuelang.org/go/cue/cuecontext" - "github.com/fluxcd/pkg/ssa" - "github.com/go-logr/logr" "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" "github.com/stefanprodan/timoni/internal/engine" @@ -224,155 +220,13 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return describeErr(f.GetModuleRoot(), "build failed", err) } - finalValues, err := builder.GetDefaultValues() - if err != nil { - return fmt.Errorf("failed to extract values: %w", err) + bi := &engine.BundleInstance{ + Name: applyArgs.name, + Namespace: *kubeconfigArgs.Namespace, + Module: *mod, } - - applySets, err := builder.GetApplySets(buildResult) - if err != nil { - return fmt.Errorf("failed to extract objects: %w", err) - } - - var objects []*unstructured.Unstructured - for _, set := range applySets { - objects = append(objects, set.Objects...) - } - - rm, err := runtime.NewResourceManager(kubeconfigArgs) - if err != nil { - return err - } - - rm.SetOwnerLabels(objects, applyArgs.name, *kubeconfigArgs.Namespace) - ctx, cancel := context.WithTimeout(cmd.Context(), rootArgs.timeout) defer cancel() - exists := false - sm := runtime.NewStorageManager(rm) - instance, err := sm.Get(ctx, applyArgs.name, *kubeconfigArgs.Namespace) - if err == nil { - exists = true - } - - nsExists, err := sm.NamespaceExists(ctx, *kubeconfigArgs.Namespace) - if err != nil { - return fmt.Errorf("instance init failed: %w", err) - } - - if !applyArgs.overwriteOwnership && exists { - err = instanceOwnershipConflicts(*instance) - if err != nil { - return err - } - } - - im := runtime.NewInstanceManager(applyArgs.name, *kubeconfigArgs.Namespace, finalValues, *mod) - - if err := im.AddObjects(objects); err != nil { - return fmt.Errorf("adding objects to instance failed: %w", err) - } - - staleObjects, err := sm.GetStaleObjects(ctx, &im.Instance) - if err != nil { - return fmt.Errorf("getting stale objects failed: %w", err) - } - - if applyArgs.dryrun || applyArgs.diff { - if !nsExists { - log.Info(colorizeJoin(colorizeNamespaceFromArgs(), ssa.CreatedAction, dryRunServer)) - } - return instanceDryRunDiff(logr.NewContext(ctx, log), rm, objects, staleObjects, nsExists, tmpDir, applyArgs.diff) - } - - if !exists { - log.Info(fmt.Sprintf("installing %s in namespace %s", applyArgs.name, *kubeconfigArgs.Namespace)) - - if err := sm.Apply(ctx, &im.Instance, true); err != nil { - return fmt.Errorf("instance init failed: %w", err) - } - - if !nsExists { - log.Info(colorizeJoin(colorizeNamespaceFromArgs(), ssa.CreatedAction)) - } - } else { - log.Info(fmt.Sprintf("upgrading %s in namespace %s", applyArgs.name, *kubeconfigArgs.Namespace)) - } - - applyOpts := runtime.ApplyOptions(applyArgs.force, rootArgs.timeout) - applyOpts.WaitInterval = 5 * time.Second - - waitOptions := ssa.WaitOptions{ - Interval: applyOpts.WaitInterval, - Timeout: rootArgs.timeout, - FailFast: true, - } - - for _, set := range applySets { - if len(applySets) > 1 { - log.Info(fmt.Sprintf("applying %s", set.Name)) - } - - cs, err := rm.ApplyAllStaged(ctx, set.Objects, applyOpts) - if err != nil { - return err - } - for _, change := range cs.Entries { - log.Info(colorizeJoin(change)) - } - - if applyArgs.wait { - spin := StartSpinner(fmt.Sprintf("waiting for %v resource(s) to become ready...", len(set.Objects))) - err = rm.Wait(set.Objects, waitOptions) - spin.Stop() - if err != nil { - return err - } - log.Info("resources are ready") - } - } - - if images, err := builder.GetContainerImages(buildResult); err == nil { - im.Instance.Images = images - } - - if err := sm.Apply(ctx, &im.Instance, true); err != nil { - return fmt.Errorf("storing instance failed: %w", err) - } - - var deletedObjects []*unstructured.Unstructured - if len(staleObjects) > 0 { - deleteOpts := runtime.DeleteOptions(applyArgs.name, *kubeconfigArgs.Namespace) - changeSet, err := rm.DeleteAll(ctx, staleObjects, deleteOpts) - if err != nil { - return fmt.Errorf("pruning objects failed: %w", err) - } - deletedObjects = runtime.SelectObjectsFromSet(changeSet, ssa.DeletedAction) - for _, change := range changeSet.Entries { - log.Info(colorizeJoin(change)) - } - } - - if applyArgs.wait { - if len(deletedObjects) > 0 { - spin := StartSpinner(fmt.Sprintf("waiting for %v resource(s) to be finalized...", len(deletedObjects))) - err = rm.WaitForTermination(deletedObjects, waitOptions) - spin.Stop() - if err != nil { - return fmt.Errorf("waiting for termination failed: %w", err) - } - - log.Info("all resources are ready") - } - } - - return nil -} - -func instanceOwnershipConflicts(instance apiv1.Instance) error { - if currentOwnerBundle := instance.Labels[apiv1.BundleNameLabelKey]; currentOwnerBundle != "" { - return fmt.Errorf("instance ownership conflict encountered. Apply with \"--overwrite-ownership\" to gain instance ownership. Conflict: instance \"%s\" exists and is managed by bundle \"%s\"", instance.Name, currentOwnerBundle) - } - return nil + return applyInstance(ctx, log, builder, buildResult, bi, tmpDir) } diff --git a/cmd/timoni/bundle_apply.go b/cmd/timoni/bundle_apply.go index a7b6da9e..a6274c51 100644 --- a/cmd/timoni/bundle_apply.go +++ b/cmd/timoni/bundle_apply.go @@ -320,6 +320,8 @@ func applyBundleInstance(ctx context.Context, cuectx *cue.Context, instance *eng } func applyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleBuilder, buildResult cue.Value, instance *engine.BundleInstance, rootDir string) error { + isStandaloneInstance := instance.Bundle != "" + finalValues, err := builder.GetDefaultValues() if err != nil { return fmt.Errorf("failed to extract values: %w", err) @@ -344,7 +346,8 @@ func applyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleB exists := false sm := runtime.NewStorageManager(rm) - if _, err = sm.Get(ctx, instance.Name, instance.Namespace); err == nil { + storedInstance, err := sm.Get(ctx, instance.Name, instance.Namespace) + if err == nil { exists = true } @@ -353,12 +356,20 @@ func applyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleB return fmt.Errorf("instance init failed: %w", err) } + if !applyArgs.overwriteOwnership && exists && isStandaloneInstance { + if currentOwnerBundle := storedInstance.Labels[apiv1.BundleNameLabelKey]; currentOwnerBundle != "" { + return instancesOwnershipConflictsErr(fmt.Sprintf("instance \"%s\" exists and is managed by bundle \"%s\"", instance.Name, currentOwnerBundle)) + } + } + im := runtime.NewInstanceManager(instance.Name, instance.Namespace, finalValues, instance.Module) - if im.Instance.Labels == nil { - im.Instance.Labels = make(map[string]string) + if isStandaloneInstance { + if im.Instance.Labels == nil { + im.Instance.Labels = make(map[string]string) + } + im.Instance.Labels[apiv1.BundleNameLabelKey] = instance.Bundle } - im.Instance.Labels[apiv1.BundleNameLabelKey] = instance.Bundle if err := im.AddObjects(objects); err != nil { return fmt.Errorf("adding objects to instance failed: %w", err) @@ -496,7 +507,7 @@ func bundleInstancesOwnershipConflicts(bundleInstances []*engine.BundleInstance) } } if len(conflicts) > 0 { - return fmt.Errorf("instance ownership conflicts encountered. Apply with \"--overwrite-ownership\" to gain instance ownership. Conflicts: %s", strings.Join(conflicts, "; ")) + return instancesOwnershipConflictsErr(strings.Join(conflicts, "; ")) } return nil @@ -516,3 +527,7 @@ func saveReaderToFile(reader io.Reader) (string, error) { return f.Name(), nil } + +func instancesOwnershipConflictsErr(msg string) error { + return errors.New("instance ownership conflict encountered. Apply with \"--overwrite-ownership\" to gain instance ownership. Conflict: " + msg) +} From d33ffb6282efeadb49906c656ef97aa21f6de2ec Mon Sep 17 00:00:00 2001 From: Ilya Dmitrichenko Date: Tue, 12 Mar 2024 14:26:36 +0000 Subject: [PATCH 03/10] Get rid of global state from `applyInstance` (WIP) Signed-off-by: Ilya Dmitrichenko --- cmd/timoni/apply.go | 17 ++++++++++++++--- cmd/timoni/bundle_apply.go | 37 +++++++++++++++++++++++++++---------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/cmd/timoni/apply.go b/cmd/timoni/apply.go index 6eadbfb6..8370f7af 100644 --- a/cmd/timoni/apply.go +++ b/cmd/timoni/apply.go @@ -220,13 +220,24 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { return describeErr(f.GetModuleRoot(), "build failed", err) } + ctx, cancel := context.WithTimeout(cmd.Context(), rootArgs.timeout) + defer cancel() + + opts := applyInstanceOptions{ + rootDir: tmpDir, + dryrun: applyArgs.diff, + diff: applyArgs.diff, + wait: applyArgs.wait, + force: applyArgs.force, + overwriteOwnership: applyArgs.overwriteOwnership, + } + bi := &engine.BundleInstance{ Name: applyArgs.name, Namespace: *kubeconfigArgs.Namespace, Module: *mod, + Bundle: "", } - ctx, cancel := context.WithTimeout(cmd.Context(), rootArgs.timeout) - defer cancel() - return applyInstance(ctx, log, builder, buildResult, bi, tmpDir) + return applyInstance(ctx, log, builder, buildResult, bi, opts, rootArgs.timeout) } diff --git a/cmd/timoni/bundle_apply.go b/cmd/timoni/bundle_apply.go index a6274c51..089a96c2 100644 --- a/cmd/timoni/bundle_apply.go +++ b/cmd/timoni/bundle_apply.go @@ -316,10 +316,27 @@ func applyBundleInstance(ctx context.Context, cuectx *cue.Context, instance *eng return describeErr(modDir, "build failed for "+instance.Name, err) } - return applyInstance(ctx, log, builder, buildResult, instance, rootDir) + opts := applyInstanceOptions{ + rootDir: rootDir, + dryrun: bundleApplyArgs.diff, + diff: bundleApplyArgs.diff, + wait: bundleApplyArgs.wait, + force: bundleApplyArgs.force, + overwriteOwnership: bundleApplyArgs.overwriteOwnership, + } + return applyInstance(ctx, log, builder, buildResult, instance, opts, rootArgs.timeout) } -func applyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleBuilder, buildResult cue.Value, instance *engine.BundleInstance, rootDir string) error { +type applyInstanceOptions struct { + rootDir string + dryrun bool + diff bool + wait bool + force bool + overwriteOwnership bool +} + +func applyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleBuilder, buildResult cue.Value, instance *engine.BundleInstance, opts applyInstanceOptions, timeout time.Duration) error { isStandaloneInstance := instance.Bundle != "" finalValues, err := builder.GetDefaultValues() @@ -356,7 +373,7 @@ func applyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleB return fmt.Errorf("instance init failed: %w", err) } - if !applyArgs.overwriteOwnership && exists && isStandaloneInstance { + if !opts.overwriteOwnership && exists && isStandaloneInstance { if currentOwnerBundle := storedInstance.Labels[apiv1.BundleNameLabelKey]; currentOwnerBundle != "" { return instancesOwnershipConflictsErr(fmt.Sprintf("instance \"%s\" exists and is managed by bundle \"%s\"", instance.Name, currentOwnerBundle)) } @@ -380,7 +397,7 @@ func applyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleB return fmt.Errorf("getting stale objects failed: %w", err) } - if bundleApplyArgs.dryrun || bundleApplyArgs.diff { + if opts.dryrun || opts.diff { if !nsExists { log.Info(colorizeJoin(colorizeSubject("Namespace/"+instance.Namespace), ssa.CreatedAction, dryRunServer)) @@ -391,8 +408,8 @@ func applyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleB objects, staleObjects, nsExists, - rootDir, - bundleApplyArgs.diff, + opts.rootDir, + opts.diff, ); err != nil { return err } @@ -417,12 +434,12 @@ func applyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleB colorizeSubject(instance.Name), colorizeSubject(instance.Namespace))) } - applyOpts := runtime.ApplyOptions(bundleApplyArgs.force, rootArgs.timeout) + applyOpts := runtime.ApplyOptions(opts.force, timeout) applyOpts.WaitInterval = 5 * time.Second waitOptions := ssa.WaitOptions{ Interval: applyOpts.WaitInterval, - Timeout: rootArgs.timeout, + Timeout: timeout, FailFast: true, } @@ -439,7 +456,7 @@ func applyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleB log.Info(colorizeJoin(change)) } - if bundleApplyArgs.wait { + if opts.wait { spin := StartSpinner(fmt.Sprintf("waiting for %v resource(s) to become ready...", len(set.Objects))) err = rm.Wait(set.Objects, waitOptions) spin.Stop() @@ -471,7 +488,7 @@ func applyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleB } } - if bundleApplyArgs.wait { + if opts.wait { if len(deletedObjects) > 0 { spin := StartSpinner(fmt.Sprintf("waiting for %v resource(s) to be finalized...", len(deletedObjects))) err = rm.WaitForTermination(deletedObjects, waitOptions) From a7181f502d3735ff975d4c5ed7aedd2327c721d3 Mon Sep 17 00:00:00 2001 From: Ilya Dmitrichenko Date: Tue, 12 Mar 2024 15:01:35 +0000 Subject: [PATCH 04/10] Correct `isStandaloneInstance` logic Signed-off-by: Ilya Dmitrichenko --- cmd/timoni/bundle_apply.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/timoni/bundle_apply.go b/cmd/timoni/bundle_apply.go index 089a96c2..2ca33482 100644 --- a/cmd/timoni/bundle_apply.go +++ b/cmd/timoni/bundle_apply.go @@ -337,7 +337,7 @@ type applyInstanceOptions struct { } func applyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleBuilder, buildResult cue.Value, instance *engine.BundleInstance, opts applyInstanceOptions, timeout time.Duration) error { - isStandaloneInstance := instance.Bundle != "" + isStandaloneInstance := instance.Bundle == "" finalValues, err := builder.GetDefaultValues() if err != nil { @@ -381,7 +381,7 @@ func applyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleB im := runtime.NewInstanceManager(instance.Name, instance.Namespace, finalValues, instance.Module) - if isStandaloneInstance { + if !isStandaloneInstance { if im.Instance.Labels == nil { im.Instance.Labels = make(map[string]string) } From 0ff53bc4167869f3afb2f8cf50cdd361f57db483 Mon Sep 17 00:00:00 2001 From: Ilya Dmitrichenko Date: Tue, 12 Mar 2024 16:28:00 +0000 Subject: [PATCH 05/10] Create `internal/{apply,dyff,log}` (WIP) Signed-off-by: Ilya Dmitrichenko --- cmd/timoni/apply.go | 26 +- cmd/timoni/artifact_list.go | 3 +- cmd/timoni/artifact_pull.go | 7 +- cmd/timoni/artifact_push.go | 9 +- cmd/timoni/artifact_tag.go | 7 +- cmd/timoni/bundle_apply.go | 236 +++-------------- cmd/timoni/bundle_delete.go | 13 +- cmd/timoni/bundle_status.go | 21 +- cmd/timoni/bundle_vet.go | 7 +- cmd/timoni/delete.go | 9 +- cmd/timoni/inspect_resources.go | 2 +- cmd/timoni/main.go | 18 +- cmd/timoni/main_test.go | 4 +- cmd/timoni/mod_init.go | 5 +- cmd/timoni/mod_list.go | 3 +- cmd/timoni/mod_pull.go | 7 +- cmd/timoni/mod_push.go | 9 +- cmd/timoni/mod_vendor_crd.go | 7 +- cmd/timoni/mod_vendor_k8s.go | 7 +- cmd/timoni/mod_vet.go | 11 +- cmd/timoni/runtime_build.go | 5 +- cmd/timoni/status.go | 19 +- internal/apply/apply.go | 245 ++++++++++++++++++ {cmd/timoni => internal/dyff}/dyff.go | 28 +- {cmd/timoni => internal/dyff}/dyff_test.go | 4 +- .../log.go => internal/logger/logger.go | 131 +++++----- 26 files changed, 472 insertions(+), 371 deletions(-) create mode 100644 internal/apply/apply.go rename {cmd/timoni => internal/dyff}/dyff.go (80%) rename {cmd/timoni => internal/dyff}/dyff_test.go (95%) rename cmd/timoni/log.go => internal/logger/logger.go (67%) diff --git a/cmd/timoni/apply.go b/cmd/timoni/apply.go index 8370f7af..fe52ba69 100644 --- a/cmd/timoni/apply.go +++ b/cmd/timoni/apply.go @@ -27,9 +27,11 @@ import ( "github.com/spf13/cobra" apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" + "github.com/stefanprodan/timoni/internal/apply" "github.com/stefanprodan/timoni/internal/engine" "github.com/stefanprodan/timoni/internal/engine/fetcher" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/runtime" ) @@ -111,6 +113,8 @@ type applyFlags struct { var applyArgs applyFlags +const ownershipConflictHint = "Apply with \"--overwrite-ownership\" to gain instance ownership." + func init() { applyCmd.Flags().VarP(&applyArgs.version, applyArgs.version.Type(), applyArgs.version.Shorthand(), applyArgs.version.Description()) applyCmd.Flags().VarP(&applyArgs.pkg, applyArgs.pkg.Type(), applyArgs.pkg.Shorthand(), applyArgs.pkg.Description()) @@ -138,7 +142,7 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { applyArgs.name = args[0] applyArgs.module = args[1] - log := LoggerInstance(cmd.Context(), applyArgs.name) + log := logger.LoggerInstance(cmd.Context(), applyArgs.name, true) version := applyArgs.version.String() if version == "" { @@ -223,13 +227,17 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(cmd.Context(), rootArgs.timeout) defer cancel() - opts := applyInstanceOptions{ - rootDir: tmpDir, - dryrun: applyArgs.diff, - diff: applyArgs.diff, - wait: applyArgs.wait, - force: applyArgs.force, - overwriteOwnership: applyArgs.overwriteOwnership, + opts := apply.Options{ + Dir: tmpDir, + DryRun: applyArgs.dryrun, + Diff: applyArgs.diff, + Wait: applyArgs.wait, + Force: applyArgs.force, + OverwriteOwnership: applyArgs.overwriteOwnership, + DiffOutput: cmd.OutOrStdout(), + KubeConfigFlags: kubeconfigArgs, + OwnershipConflictHint: ownershipConflictHint, + // ProgressStart: logger.StartSpinner, } bi := &engine.BundleInstance{ @@ -239,5 +247,5 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { Bundle: "", } - return applyInstance(ctx, log, builder, buildResult, bi, opts, rootArgs.timeout) + return apply.ApplyInstance(ctx, log, builder, buildResult, bi, opts, rootArgs.timeout) } diff --git a/cmd/timoni/artifact_list.go b/cmd/timoni/artifact_list.go index 1b9b8744..a2b2ff40 100644 --- a/cmd/timoni/artifact_list.go +++ b/cmd/timoni/artifact_list.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/cobra" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/oci" ) @@ -64,7 +65,7 @@ func listArtifactCmdRun(cmd *cobra.Command, args []string) error { } ociURL := args[0] - spin := StartSpinner("fetching tags") + spin := logger.StartSpinner("fetching tags") defer spin.Stop() ctx, cancel := context.WithTimeout(cmd.Context(), rootArgs.timeout) diff --git a/cmd/timoni/artifact_pull.go b/cmd/timoni/artifact_pull.go index d98b5db8..f7e16923 100644 --- a/cmd/timoni/artifact_pull.go +++ b/cmd/timoni/artifact_pull.go @@ -24,6 +24,7 @@ import ( "github.com/spf13/cobra" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/oci" ) @@ -105,7 +106,7 @@ func pullArtifactCmdRun(cmd *cobra.Command, args []string) error { } ociURL := args[0] - log := LoggerFrom(cmd.Context()) + log := logger.LoggerFrom(cmd.Context()) if err := os.MkdirAll(pullArtifactArgs.output, os.ModePerm); err != nil { return fmt.Errorf("invalid output path %s: %w", pullArtifactArgs.output, err) @@ -125,7 +126,7 @@ func pullArtifactCmdRun(cmd *cobra.Command, args []string) error { } } - spin := StartSpinner("pulling artifact") + spin := logger.StartSpinner("pulling artifact") defer spin.Stop() ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) @@ -138,7 +139,7 @@ func pullArtifactCmdRun(cmd *cobra.Command, args []string) error { } spin.Stop() - log.Info(fmt.Sprintf("extracted: %s", colorizeSubject(pullArtifactArgs.output))) + log.Info(fmt.Sprintf("extracted: %s", logger.ColorizeSubject(pullArtifactArgs.output))) return nil } diff --git a/cmd/timoni/artifact_push.go b/cmd/timoni/artifact_push.go index 45f6cc5b..ca7376e4 100644 --- a/cmd/timoni/artifact_push.go +++ b/cmd/timoni/artifact_push.go @@ -26,6 +26,7 @@ import ( apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" "github.com/stefanprodan/timoni/internal/engine" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/oci" ) @@ -118,7 +119,7 @@ func pushArtifactCmdRun(cmd *cobra.Command, args []string) error { pushArtifactArgs.ignorePaths = append(pushArtifactArgs.ignorePaths, ps...) } - log := LoggerFrom(cmd.Context()) + log := logger.LoggerFrom(cmd.Context()) ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() @@ -128,7 +129,7 @@ func pushArtifactCmdRun(cmd *cobra.Command, args []string) error { } oci.AppendGitMetadata(pushArtifactArgs.path, annotations) - spin := StartSpinner("pushing artifact") + spin := logger.StartSpinner("pushing artifact") defer spin.Stop() opts := oci.Options(ctx, pushArtifactArgs.creds.String(), rootArgs.registryInsecure) @@ -164,8 +165,8 @@ func pushArtifactCmdRun(cmd *cobra.Command, args []string) error { if err != nil { return err } - log.Info(fmt.Sprintf("artifact: %s", colorizeSubject(ociURL))) - log.Info(fmt.Sprintf("digest: %s", colorizeSubject(digest.DigestStr()))) + log.Info(fmt.Sprintf("artifact: %s", logger.ColorizeSubject(ociURL))) + log.Info(fmt.Sprintf("digest: %s", logger.ColorizeSubject(digest.DigestStr()))) return nil } diff --git a/cmd/timoni/artifact_tag.go b/cmd/timoni/artifact_tag.go index 32c6ce81..24067a79 100644 --- a/cmd/timoni/artifact_tag.go +++ b/cmd/timoni/artifact_tag.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/cobra" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/oci" ) @@ -62,10 +63,10 @@ func tagArtifactCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("at least one tag is required") } - spin := StartSpinner("tagging artifact") + spin := logger.StartSpinner("tagging artifact") defer spin.Stop() - log := LoggerFrom(cmd.Context()) + log := logger.LoggerFrom(cmd.Context()) ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() @@ -86,7 +87,7 @@ func tagArtifactCmdRun(cmd *cobra.Command, args []string) error { } for _, tag := range tagArtifactArgs.tags { - log.Info(fmt.Sprintf("tagged: %s", colorizeSubject(fmt.Sprintf("%s:%s", baseURL, tag)))) + log.Info(fmt.Sprintf("tagged: %s", logger.ColorizeSubject(fmt.Sprintf("%s:%s", baseURL, tag)))) } return nil diff --git a/cmd/timoni/bundle_apply.go b/cmd/timoni/bundle_apply.go index 2ca33482..e1913d7e 100644 --- a/cmd/timoni/bundle_apply.go +++ b/cmd/timoni/bundle_apply.go @@ -29,15 +29,15 @@ import ( "cuelang.org/go/cue" "cuelang.org/go/cue/cuecontext" - "github.com/fluxcd/pkg/ssa" "github.com/go-logr/logr" "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" + "github.com/stefanprodan/timoni/internal/apply" "github.com/stefanprodan/timoni/internal/engine" "github.com/stefanprodan/timoni/internal/engine/fetcher" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/runtime" ) @@ -191,7 +191,7 @@ func runBundleApplyCmd(cmd *cobra.Command, _ []string) error { return err } - log := LoggerBundle(cmd.Context(), bundle.Name, cluster.Name) + log := logger.LoggerBundle(cmd.Context(), bundle.Name, cluster.Name, true) if !bundleApplyArgs.overwriteOwnership { err = bundleInstancesOwnershipConflicts(bundle.Instances) @@ -201,7 +201,7 @@ func runBundleApplyCmd(cmd *cobra.Command, _ []string) error { } for _, instance := range bundle.Instances { - spin := StartSpinner(fmt.Sprintf("pulling %s", instance.Module.Repository)) + spin := logger.StartSpinner(fmt.Sprintf("pulling %s", instance.Module.Repository)) pullErr := fetchBundleInstanceModule(ctxPull, instance, tmpDir) spin.Stop() if pullErr != nil { @@ -216,18 +216,18 @@ func runBundleApplyCmd(cmd *cobra.Command, _ []string) error { startMsg := fmt.Sprintf("applying %v instance(s)", len(bundle.Instances)) if !cluster.IsDefault() { - startMsg = fmt.Sprintf("%s on %s", startMsg, colorizeSubject(cluster.Group)) + startMsg = fmt.Sprintf("%s on %s", startMsg, logger.ColorizeSubject(cluster.Group)) } if bundleApplyArgs.dryrun || bundleApplyArgs.diff { - log.Info(fmt.Sprintf("%s %s", startMsg, colorizeDryRun("(server dry run)"))) + log.Info(fmt.Sprintf("%s %s", startMsg, logger.ColorizeDryRun("(server dry run)"))) } else { log.Info(startMsg) } for _, instance := range bundle.Instances { instance.Cluster = cluster.Name - if err := applyBundleInstance(logr.NewContext(ctx, log), cuectx, instance, kubeVersion, tmpDir); err != nil { + if err := applyBundleInstance(logr.NewContext(ctx, log), cuectx, instance, kubeVersion, tmpDir, cmd.OutOrStdout()); err != nil { return err } } @@ -235,7 +235,7 @@ func runBundleApplyCmd(cmd *cobra.Command, _ []string) error { elapsed := time.Since(start) if bundleApplyArgs.dryrun || bundleApplyArgs.diff { log.Info(fmt.Sprintf("applied successfully %s", - colorizeDryRun("(server dry run)"))) + logger.ColorizeDryRun("(server dry run)"))) } else { log.Info(fmt.Sprintf("applied successfully in %s", elapsed.Round(time.Second))) } @@ -280,8 +280,8 @@ func fetchBundleInstanceModule(ctx context.Context, instance *engine.BundleInsta return nil } -func applyBundleInstance(ctx context.Context, cuectx *cue.Context, instance *engine.BundleInstance, kubeVersion string, rootDir string) error { - log := LoggerBundleInstance(ctx, instance.Bundle, instance.Cluster, instance.Name) +func applyBundleInstance(ctx context.Context, cuectx *cue.Context, instance *engine.BundleInstance, kubeVersion string, rootDir string, diffOutput io.Writer) error { + log := logger.LoggerBundleInstance(ctx, instance.Bundle, instance.Cluster, instance.Name, true) modDir := path.Join(rootDir, instance.Name, "module") builder := engine.NewModuleBuilder( @@ -303,7 +303,7 @@ func applyBundleInstance(ctx context.Context, cuectx *cue.Context, instance *eng instance.Module.Name = modName log.Info(fmt.Sprintf("applying module %s version %s", - colorizeSubject(instance.Module.Name), colorizeSubject(instance.Module.Version))) + logger.ColorizeSubject(instance.Module.Name), logger.ColorizeSubject(instance.Module.Version))) err = builder.WriteValuesFileWithDefaults(instance.Values) if err != nil { return err @@ -316,190 +316,35 @@ func applyBundleInstance(ctx context.Context, cuectx *cue.Context, instance *eng return describeErr(modDir, "build failed for "+instance.Name, err) } - opts := applyInstanceOptions{ - rootDir: rootDir, - dryrun: bundleApplyArgs.diff, - diff: bundleApplyArgs.diff, - wait: bundleApplyArgs.wait, - force: bundleApplyArgs.force, - overwriteOwnership: bundleApplyArgs.overwriteOwnership, + opts := apply.Options{ + Dir: rootDir, + DryRun: bundleApplyArgs.dryrun, + Diff: bundleApplyArgs.diff, + Wait: bundleApplyArgs.wait, + Force: bundleApplyArgs.force, + OverwriteOwnership: bundleApplyArgs.overwriteOwnership, + DiffOutput: diffOutput, + KubeConfigFlags: kubeconfigArgs, + OwnershipConflictHint: ownershipConflictHint, + // ProgressStart: logger.StartSpinner, } - return applyInstance(ctx, log, builder, buildResult, instance, opts, rootArgs.timeout) -} -type applyInstanceOptions struct { - rootDir string - dryrun bool - diff bool - wait bool - force bool - overwriteOwnership bool + return apply.ApplyInstance(ctx, log, builder, buildResult, instance, opts, rootArgs.timeout) } -func applyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleBuilder, buildResult cue.Value, instance *engine.BundleInstance, opts applyInstanceOptions, timeout time.Duration) error { - isStandaloneInstance := instance.Bundle == "" - - finalValues, err := builder.GetDefaultValues() - if err != nil { - return fmt.Errorf("failed to extract values: %w", err) - } - - sets, err := builder.GetApplySets(buildResult) - if err != nil { - return fmt.Errorf("failed to extract objects: %w", err) - } - - var objects []*unstructured.Unstructured - for _, set := range sets { - objects = append(objects, set.Objects...) - } - - rm, err := runtime.NewResourceManager(kubeconfigArgs) - if err != nil { - return err - } - - rm.SetOwnerLabels(objects, instance.Name, instance.Namespace) - - exists := false - sm := runtime.NewStorageManager(rm) - storedInstance, err := sm.Get(ctx, instance.Name, instance.Namespace) - if err == nil { - exists = true - } - - nsExists, err := sm.NamespaceExists(ctx, instance.Namespace) - if err != nil { - return fmt.Errorf("instance init failed: %w", err) - } - - if !opts.overwriteOwnership && exists && isStandaloneInstance { - if currentOwnerBundle := storedInstance.Labels[apiv1.BundleNameLabelKey]; currentOwnerBundle != "" { - return instancesOwnershipConflictsErr(fmt.Sprintf("instance \"%s\" exists and is managed by bundle \"%s\"", instance.Name, currentOwnerBundle)) - } - } - - im := runtime.NewInstanceManager(instance.Name, instance.Namespace, finalValues, instance.Module) - - if !isStandaloneInstance { - if im.Instance.Labels == nil { - im.Instance.Labels = make(map[string]string) - } - im.Instance.Labels[apiv1.BundleNameLabelKey] = instance.Bundle - } - - if err := im.AddObjects(objects); err != nil { - return fmt.Errorf("adding objects to instance failed: %w", err) - } - - staleObjects, err := sm.GetStaleObjects(ctx, &im.Instance) +func saveReaderToFile(reader io.Reader) (string, error) { + f, err := os.CreateTemp("", "*.cue") if err != nil { - return fmt.Errorf("getting stale objects failed: %w", err) - } - - if opts.dryrun || opts.diff { - if !nsExists { - log.Info(colorizeJoin(colorizeSubject("Namespace/"+instance.Namespace), - ssa.CreatedAction, dryRunServer)) - } - if err := instanceDryRunDiff( - logr.NewContext(ctx, log), - rm, - objects, - staleObjects, - nsExists, - opts.rootDir, - opts.diff, - ); err != nil { - return err - } - - log.Info(colorizeJoin("applied successfully", colorizeDryRun("(server dry run)"))) - return nil - } - - if !exists { - log.Info(fmt.Sprintf("installing %s in namespace %s", - colorizeSubject(instance.Name), colorizeSubject(instance.Namespace))) - - if err := sm.Apply(ctx, &im.Instance, true); err != nil { - return fmt.Errorf("instance init failed: %w", err) - } - - if !nsExists { - log.Info(colorizeJoin(colorizeSubject("Namespace/"+instance.Namespace), ssa.CreatedAction)) - } - } else { - log.Info(fmt.Sprintf("upgrading %s in namespace %s", - colorizeSubject(instance.Name), colorizeSubject(instance.Namespace))) - } - - applyOpts := runtime.ApplyOptions(opts.force, timeout) - applyOpts.WaitInterval = 5 * time.Second - - waitOptions := ssa.WaitOptions{ - Interval: applyOpts.WaitInterval, - Timeout: timeout, - FailFast: true, - } - - for _, set := range sets { - if len(sets) > 1 { - log.Info(fmt.Sprintf("applying %s", set.Name)) - } - - cs, err := rm.ApplyAllStaged(ctx, set.Objects, applyOpts) - if err != nil { - return err - } - for _, change := range cs.Entries { - log.Info(colorizeJoin(change)) - } - - if opts.wait { - spin := StartSpinner(fmt.Sprintf("waiting for %v resource(s) to become ready...", len(set.Objects))) - err = rm.Wait(set.Objects, waitOptions) - spin.Stop() - if err != nil { - return err - } - log.Info(fmt.Sprintf("%s resources %s", set.Name, colorizeReady("ready"))) - } - } - - if images, err := builder.GetContainerImages(buildResult); err == nil { - im.Instance.Images = images - } - - if err := sm.Apply(ctx, &im.Instance, true); err != nil { - return fmt.Errorf("storing instance failed: %w", err) + return "", errors.New("unable to create temp dir for stdin") } - var deletedObjects []*unstructured.Unstructured - if len(staleObjects) > 0 { - deleteOpts := runtime.DeleteOptions(instance.Name, instance.Namespace) - changeSet, err := rm.DeleteAll(ctx, staleObjects, deleteOpts) - if err != nil { - return fmt.Errorf("pruning objects failed: %w", err) - } - deletedObjects = runtime.SelectObjectsFromSet(changeSet, ssa.DeletedAction) - for _, change := range changeSet.Entries { - log.Info(colorizeJoin(change)) - } - } + defer f.Close() - if opts.wait { - if len(deletedObjects) > 0 { - spin := StartSpinner(fmt.Sprintf("waiting for %v resource(s) to be finalized...", len(deletedObjects))) - err = rm.WaitForTermination(deletedObjects, waitOptions) - spin.Stop() - if err != nil { - return fmt.Errorf("waiting for termination failed: %w", err) - } - } + if _, err := io.Copy(f, reader); err != nil { + return "", fmt.Errorf("error writing stdin to file: %w", err) } - return nil + return f.Name(), nil } func bundleInstancesOwnershipConflicts(bundleInstances []*engine.BundleInstance) error { @@ -524,27 +369,8 @@ func bundleInstancesOwnershipConflicts(bundleInstances []*engine.BundleInstance) } } if len(conflicts) > 0 { - return instancesOwnershipConflictsErr(strings.Join(conflicts, "; ")) + return apply.InstanceOwnershipConflictsErr(strings.Join(conflicts, "; "), ownershipConflictHint) } return nil } - -func saveReaderToFile(reader io.Reader) (string, error) { - f, err := os.CreateTemp("", "*.cue") - if err != nil { - return "", errors.New("unable to create temp dir for stdin") - } - - defer f.Close() - - if _, err := io.Copy(f, reader); err != nil { - return "", fmt.Errorf("error writing stdin to file: %w", err) - } - - return f.Name(), nil -} - -func instancesOwnershipConflictsErr(msg string) error { - return errors.New("instance ownership conflict encountered. Apply with \"--overwrite-ownership\" to gain instance ownership. Conflict: " + msg) -} diff --git a/cmd/timoni/bundle_delete.go b/cmd/timoni/bundle_delete.go index 647124e1..8160271a 100644 --- a/cmd/timoni/bundle_delete.go +++ b/cmd/timoni/bundle_delete.go @@ -29,6 +29,7 @@ import ( apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" "github.com/stefanprodan/timoni/internal/engine" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/runtime" ) @@ -120,7 +121,7 @@ func runBundleDelCmd(cmd *cobra.Command, args []string) error { return err } - log := LoggerBundle(ctx, bundleDelArgs.name, cluster.Name) + log := logger.LoggerBundle(ctx, bundleDelArgs.name, cluster.Name, true) if len(instances) == 0 { log.Error(nil, "no instances found in bundle") @@ -131,7 +132,7 @@ func runBundleDelCmd(cmd *cobra.Command, args []string) error { for index := len(instances) - 1; index >= 0; index-- { instance := instances[index] log.Info(fmt.Sprintf("deleting instance %s in namespace %s", - colorizeSubject(instance.Name), colorizeSubject(instance.Namespace))) + logger.ColorizeSubject(instance.Name), logger.ColorizeSubject(instance.Namespace))) if err := deleteBundleInstance(ctx, &engine.BundleInstance{ Bundle: bundleDelArgs.name, Cluster: cluster.Name, @@ -146,7 +147,7 @@ func runBundleDelCmd(cmd *cobra.Command, args []string) error { } func deleteBundleInstance(ctx context.Context, instance *engine.BundleInstance, wait bool, dryrun bool) error { - log := LoggerBundle(ctx, instance.Bundle, instance.Cluster) + log := logger.LoggerBundle(ctx, instance.Bundle, instance.Cluster, true) sm, err := runtime.NewResourceManager(kubeconfigArgs) if err != nil { @@ -172,7 +173,7 @@ func deleteBundleInstance(ctx context.Context, instance *engine.BundleInstance, if dryrun { for _, object := range objects { - log.Info(colorizeJoin(object, ssa.DeletedAction, dryRunClient)) + log.Info(logger.ColorizeJoin(object, ssa.DeletedAction, logger.DryRunClient)) } return nil } @@ -188,7 +189,7 @@ func deleteBundleInstance(ctx context.Context, instance *engine.BundleInstance, continue } cs.Add(*change) - log.Info(colorizeJoin(change)) + log.Info(logger.ColorizeJoin(change)) } if hasErrors { @@ -203,7 +204,7 @@ func deleteBundleInstance(ctx context.Context, instance *engine.BundleInstance, if wait && len(deletedObjects) > 0 { waitOpts := ssa.DefaultWaitOptions() waitOpts.Timeout = rootArgs.timeout - spin := StartSpinner(fmt.Sprintf("waiting for %v resource(s) to be finalized...", len(deletedObjects))) + spin := logger.StartSpinner(fmt.Sprintf("waiting for %v resource(s) to be finalized...", len(deletedObjects))) err = sm.WaitForTermination(deletedObjects, waitOpts) spin.Stop() if err != nil { diff --git a/cmd/timoni/bundle_status.go b/cmd/timoni/bundle_status.go index 13e101b3..b423c75e 100644 --- a/cmd/timoni/bundle_status.go +++ b/cmd/timoni/bundle_status.go @@ -28,6 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/stefanprodan/timoni/internal/engine" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/runtime" apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" @@ -103,7 +104,7 @@ func runBundleStatusCmd(cmd *cobra.Command, args []string) error { return err } - log := LoggerBundle(ctx, bundleStatusArgs.name, cluster.Name) + log := logger.LoggerBundle(ctx, bundleStatusArgs.name, cluster.Name, true) if len(instances) == 0 { log.Error(nil, "no instances found in bundle") @@ -112,18 +113,18 @@ func runBundleStatusCmd(cmd *cobra.Command, args []string) error { } for _, instance := range instances { - log := LoggerBundleInstance(ctx, bundleStatusArgs.name, cluster.Name, instance.Name) + log := logger.LoggerBundleInstance(ctx, bundleStatusArgs.name, cluster.Name, instance.Name, true) log.Info(fmt.Sprintf("last applied %s", - colorizeSubject(instance.LastTransitionTime))) + logger.ColorizeSubject(instance.LastTransitionTime))) log.Info(fmt.Sprintf("module %s", - colorizeSubject(instance.Module.Repository+":"+instance.Module.Version))) + logger.ColorizeSubject(instance.Module.Repository+":"+instance.Module.Version))) log.Info(fmt.Sprintf("digest %s", - colorizeSubject(instance.Module.Digest))) + logger.ColorizeSubject(instance.Module.Digest))) for _, image := range instance.Images { log.Info(fmt.Sprintf("container image %s", - colorizeSubject(image))) + logger.ColorizeSubject(image))) } im := runtime.InstanceManager{Instance: apiv1.Instance{Inventory: instance.Inventory}} @@ -137,23 +138,23 @@ func runBundleStatusCmd(cmd *cobra.Command, args []string) error { err = rm.Client().Get(ctx, client.ObjectKeyFromObject(obj), obj) if err != nil { if apierrors.IsNotFound(err) { - log.Error(err, colorizeJoin(obj, errors.New("NotFound"))) + log.Error(err, logger.ColorizeJoin(obj, errors.New("NotFound"))) failed = true continue } - log.Error(err, colorizeJoin(obj, errors.New("Unknown"))) + log.Error(err, logger.ColorizeJoin(obj, errors.New("Unknown"))) failed = true continue } res, err := status.Compute(obj) if err != nil { - log.Error(err, colorizeJoin(obj, errors.New("Failed"))) + log.Error(err, logger.ColorizeJoin(obj, errors.New("Failed"))) failed = true continue } - log.Info(colorizeJoin(obj, res.Status, "-", res.Message)) + log.Info(logger.ColorizeJoin(obj, res.Status, "-", res.Message)) } } } diff --git a/cmd/timoni/bundle_vet.go b/cmd/timoni/bundle_vet.go index 0ef5760e..f36d5345 100644 --- a/cmd/timoni/bundle_vet.go +++ b/cmd/timoni/bundle_vet.go @@ -31,6 +31,7 @@ import ( apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" "github.com/stefanprodan/timoni/internal/engine" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/runtime" ) @@ -78,7 +79,7 @@ func init() { } func runBundleVetCmd(cmd *cobra.Command, args []string) error { - log := LoggerFrom(cmd.Context()) + log := logger.LoggerFrom(cmd.Context()) files := bundleVetArgs.files if len(files) == 0 { return fmt.Errorf("no bundle provided with -f") @@ -169,7 +170,7 @@ func runBundleVetCmd(cmd *cobra.Command, args []string) error { return err } - log = LoggerBundle(logr.NewContext(cmd.Context(), log), bundle.Name, apiv1.RuntimeDefaultName) + log = logger.LoggerBundle(logr.NewContext(cmd.Context(), log), bundle.Name, apiv1.RuntimeDefaultName, true) if len(bundle.Instances) == 0 { return fmt.Errorf("no instances found in bundle") @@ -193,7 +194,7 @@ func runBundleVetCmd(cmd *cobra.Command, args []string) error { if i.Namespace == "" { return fmt.Errorf("instance %s does not have a namespace", i.Name) } - log := LoggerBundleInstance(logr.NewContext(cmd.Context(), log), bundle.Name, cluster.Name, i.Name) + log := logger.LoggerBundleInstance(logr.NewContext(cmd.Context(), log), bundle.Name, cluster.Name, i.Name, true) log.Info("instance is valid") } } diff --git a/cmd/timoni/delete.go b/cmd/timoni/delete.go index 2887737d..5262dc7e 100644 --- a/cmd/timoni/delete.go +++ b/cmd/timoni/delete.go @@ -25,6 +25,7 @@ import ( "github.com/fluxcd/pkg/ssa" "github.com/spf13/cobra" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/runtime" ) @@ -72,7 +73,7 @@ func runDeleteCmd(cmd *cobra.Command, args []string) error { deleteArgs.name = args[0] - log := LoggerInstance(cmd.Context(), deleteArgs.name) + log := logger.LoggerInstance(cmd.Context(), deleteArgs.name, true) sm, err := runtime.NewResourceManager(kubeconfigArgs) if err != nil { return err @@ -97,7 +98,7 @@ func runDeleteCmd(cmd *cobra.Command, args []string) error { if deleteArgs.dryrun { for _, object := range objects { - log.Info(colorizeJoin(object, ssa.DeletedAction, dryRunClient)) + log.Info(logger.ColorizeJoin(object, ssa.DeletedAction, logger.DryRunClient)) } return nil } @@ -114,7 +115,7 @@ func runDeleteCmd(cmd *cobra.Command, args []string) error { continue } cs.Add(*change) - log.Info(colorizeJoin(change)) + log.Info(logger.ColorizeJoin(change)) } if hasErrors { @@ -129,7 +130,7 @@ func runDeleteCmd(cmd *cobra.Command, args []string) error { if deleteArgs.wait && len(deletedObjects) > 0 { waitOpts := ssa.DefaultWaitOptions() waitOpts.Timeout = rootArgs.timeout - spin := StartSpinner(fmt.Sprintf("waiting for %v resource(s) to be finalized...", len(deletedObjects))) + spin := logger.StartSpinner(fmt.Sprintf("waiting for %v resource(s) to be finalized...", len(deletedObjects))) err = sm.WaitForTermination(deletedObjects, waitOpts) spin.Stop() if err != nil { diff --git a/cmd/timoni/inspect_resources.go b/cmd/timoni/inspect_resources.go index 3e691f82..ef48e20d 100644 --- a/cmd/timoni/inspect_resources.go +++ b/cmd/timoni/inspect_resources.go @@ -81,7 +81,7 @@ func runInspectResourcesCmd(cmd *cobra.Command, args []string) error { //} // //for _, meta := range metas { - // fmt.Fprintln(cmd.OutOrStdout(), colorizeSubject(ssa.FmtObjMetadata(meta))) + // fmt.Fprintln(cmd.OutOrStdout(), logger.ColorizeSubject(ssa.FmtObjMetadata(meta))) //} objects, err := iManager.ListObjects() diff --git a/cmd/timoni/main.go b/cmd/timoni/main.go index 975cf8b9..3f5118bb 100644 --- a/cmd/timoni/main.go +++ b/cmd/timoni/main.go @@ -29,6 +29,8 @@ import ( "k8s.io/cli-runtime/pkg/genericclioptions" _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/client-go/tools/clientcmd" + + "github.com/stefanprodan/timoni/internal/logger" ) var ( @@ -46,12 +48,12 @@ var rootCmd = &cobra.Command{ // Initialize the console logger just before running // a command only if one wasn't provided. This allows other // callers (e.g. unit tests) to inject their own logger ahead of time. - if logger.IsZero() { - logger = NewConsoleLogger() + if cliLogger.IsZero() { + cliLogger = logger.NewConsoleLogger(true, true) } // Inject the logger in the command context. - ctx := logr.NewContext(context.Background(), logger) + ctx := logr.NewContext(context.Background(), cliLogger) cmd.SetContext(ctx) }, } @@ -70,7 +72,7 @@ var ( coloredLog: !color.NoColor, timeout: 5 * time.Minute, } - logger logr.Logger + cliLogger logr.Logger kubeconfigArgs = genericclioptions.NewConfigFlags(false) ) @@ -98,13 +100,13 @@ func main() { if err := rootCmd.Execute(); err != nil { // Ensure a logger is initialized even if the rootCmd // failed before running its hooks. - if logger.IsZero() { - logger = NewConsoleLogger() - } + if cliLogger.IsZero() { + cliLogger = logger.NewConsoleLogger(true, true) + } // Set the logger err to nil to pretty print // the error message on multiple lines. - logger.Error(nil, err.Error()) + cliLogger.Error(nil, err.Error()) os.Exit(1) } } diff --git a/cmd/timoni/main_test.go b/cmd/timoni/main_test.go index 123c26be..569dea39 100644 --- a/cmd/timoni/main_test.go +++ b/cmd/timoni/main_test.go @@ -107,8 +107,8 @@ func executeCommandWithIn(cmd string, in io.Reader) (string, error) { zerolog.LevelFieldName, } zl := zerolog.New(zcfg) - logger = zerologr.New(&zl) - runtimeLog.SetLogger(logger) + cliLogger = zerologr.New(&zl) + runtimeLog.SetLogger(cliLogger) _, err = rootCmd.ExecuteC() result := buf.String() diff --git a/cmd/timoni/mod_init.go b/cmd/timoni/mod_init.go index bec3a02f..2686ae4c 100644 --- a/cmd/timoni/mod_init.go +++ b/cmd/timoni/mod_init.go @@ -28,6 +28,7 @@ import ( "github.com/spf13/cobra" apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/oci" ) @@ -76,7 +77,7 @@ func runInitModCmd(cmd *cobra.Command, args []string) error { initModArgs.path = "." } - log := LoggerFrom(cmd.Context()) + log := logger.LoggerFrom(cmd.Context()) if fs, err := os.Stat(initModArgs.path); err != nil || !fs.IsDir() { return fmt.Errorf("path not found: %s", initModArgs.path) @@ -98,7 +99,7 @@ func runInitModCmd(cmd *cobra.Command, args []string) error { templateName = "blueprint" } - spin := StartSpinner(fmt.Sprintf("pulling template from %s", templateURL)) + spin := logger.StartSpinner(fmt.Sprintf("pulling template from %s", templateURL)) defer spin.Stop() opts := oci.Options(ctx, "", rootArgs.registryInsecure) diff --git a/cmd/timoni/mod_list.go b/cmd/timoni/mod_list.go index 4a101590..b88f5074 100644 --- a/cmd/timoni/mod_list.go +++ b/cmd/timoni/mod_list.go @@ -23,6 +23,7 @@ import ( "github.com/spf13/cobra" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/oci" ) @@ -64,7 +65,7 @@ func listModCmdRun(cmd *cobra.Command, args []string) error { } ociURL := args[0] - spin := StartSpinner("fetching versions") + spin := logger.StartSpinner("fetching versions") defer spin.Stop() ctx, cancel := context.WithTimeout(cmd.Context(), rootArgs.timeout) diff --git a/cmd/timoni/mod_pull.go b/cmd/timoni/mod_pull.go index 88d606aa..bdf4185d 100644 --- a/cmd/timoni/mod_pull.go +++ b/cmd/timoni/mod_pull.go @@ -26,6 +26,7 @@ import ( apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/oci" ) @@ -122,7 +123,7 @@ func pullCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid output path %s: %w", pullModArgs.output, err) } - log := LoggerFrom(cmd.Context()) + log := logger.LoggerFrom(cmd.Context()) if pullModArgs.verify != "" { err := oci.VerifyArtifact(log, @@ -141,7 +142,7 @@ func pullCmdRun(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() - spin := StartSpinner(fmt.Sprintf("pulling %s", ociURL)) + spin := logger.StartSpinner(fmt.Sprintf("pulling %s", ociURL)) opts := oci.Options(ctx, pullModArgs.creds.String(), rootArgs.registryInsecure) err := oci.PullArtifact(ociURL, pullModArgs.output, apiv1.AnyContentType, opts) spin.Stop() @@ -149,7 +150,7 @@ func pullCmdRun(cmd *cobra.Command, args []string) error { return err } - log.Info(fmt.Sprintf("extracted: %s", colorizeSubject(pullModArgs.output))) + log.Info(fmt.Sprintf("extracted: %s", logger.ColorizeSubject(pullModArgs.output))) return nil } diff --git a/cmd/timoni/mod_push.go b/cmd/timoni/mod_push.go index 7d42af8b..89ce1a7d 100644 --- a/cmd/timoni/mod_push.go +++ b/cmd/timoni/mod_push.go @@ -29,6 +29,7 @@ import ( apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" "github.com/stefanprodan/timoni/internal/engine" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/oci" ) @@ -123,7 +124,7 @@ func pushModCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("module not found at path %s", pushModArgs.module) } - log := LoggerFrom(cmd.Context()) + log := logger.LoggerFrom(cmd.Context()) annotations, err := oci.ParseAnnotations(pushModArgs.annotations) if err != nil { @@ -142,7 +143,7 @@ func pushModCmdRun(cmd *cobra.Command, args []string) error { } pushModArgs.ignorePaths = append(pushModArgs.ignorePaths, ps...) - spin := StartSpinner("pushing module") + spin := logger.StartSpinner("pushing module") defer spin.Stop() opts := oci.Options(ctx, pushModArgs.creds.String(), rootArgs.registryInsecure) @@ -201,8 +202,8 @@ func pushModCmdRun(cmd *cobra.Command, args []string) error { if err != nil { return err } - log.Info(fmt.Sprintf("artifact: %s", colorizeSubject(ociURL))) - log.Info(fmt.Sprintf("digest: %s", colorizeSubject(digest.DigestStr()))) + log.Info(fmt.Sprintf("artifact: %s", logger.ColorizeSubject(ociURL))) + log.Info(fmt.Sprintf("digest: %s", logger.ColorizeSubject(digest.DigestStr()))) } return nil diff --git a/cmd/timoni/mod_vendor_crd.go b/cmd/timoni/mod_vendor_crd.go index 0658553e..5e1b788d 100644 --- a/cmd/timoni/mod_vendor_crd.go +++ b/cmd/timoni/mod_vendor_crd.go @@ -34,6 +34,7 @@ import ( "sigs.k8s.io/yaml" "github.com/stefanprodan/timoni/internal/engine" + "github.com/stefanprodan/timoni/internal/logger" ) var vendorCrdCmd = &cobra.Command{ @@ -72,7 +73,7 @@ func runVendorCrdCmd(cmd *cobra.Command, args []string) error { vendorCrdArgs.modRoot = args[0] } - log := LoggerFrom(cmd.Context()) + log := logger.LoggerFrom(cmd.Context()) cuectx := cuecontext.New() // Make sure we're importing into a CUE module. @@ -81,7 +82,7 @@ func runVendorCrdCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("cue.mod not found in the module path %s", vendorCrdArgs.modRoot) } - spin := StartSpinner("importing schemas") + spin := logger.StartSpinner("importing schemas") defer spin.Stop() // Load the YAML manifest into memory either from disk or by fetching the file over HTTPS. @@ -160,7 +161,7 @@ func runVendorCrdCmd(cmd *cobra.Command, args []string) error { // Write the definitions to the module's 'cue.mod/gen' dir. for _, k := range keys { - log.Info(fmt.Sprintf("schemas vendored: %s", colorizeSubject(k))) + log.Info(fmt.Sprintf("schemas vendored: %s", logger.ColorizeSubject(k))) dstDir := path.Join(cueModDir, "gen", k) if err := os.MkdirAll(dstDir, os.ModePerm); err != nil { diff --git a/cmd/timoni/mod_vendor_k8s.go b/cmd/timoni/mod_vendor_k8s.go index cf55d620..2b4fc4ca 100644 --- a/cmd/timoni/mod_vendor_k8s.go +++ b/cmd/timoni/mod_vendor_k8s.go @@ -26,6 +26,7 @@ import ( "github.com/spf13/cobra" apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/oci" ) @@ -62,7 +63,7 @@ func runVendorK8sCmd(cmd *cobra.Command, args []string) error { vendorK8sArgs.modRoot = args[0] } - log := LoggerFrom(cmd.Context()) + log := logger.LoggerFrom(cmd.Context()) // Make sure we're importing into a CUE module. cueModDir := path.Join(vendorK8sArgs.modRoot, "cue.mod") @@ -78,7 +79,7 @@ func runVendorK8sCmd(cmd *cobra.Command, args []string) error { ociURL = fmt.Sprintf("%s:v%s", k8sSchemaURL, ver) } - spin := StartSpinner(fmt.Sprintf("importing schemas from %s", ociURL)) + spin := logger.StartSpinner(fmt.Sprintf("importing schemas from %s", ociURL)) defer spin.Stop() opts := oci.Options(ctx, "", rootArgs.registryInsecure) @@ -88,7 +89,7 @@ func runVendorK8sCmd(cmd *cobra.Command, args []string) error { } spin.Stop() - log.Info(fmt.Sprintf("schemas vendored: %s", colorizeSubject(path.Join(cueModDir, "gen", "k8s.io", "api")))) + log.Info(fmt.Sprintf("schemas vendored: %s", logger.ColorizeSubject(path.Join(cueModDir, "gen", "k8s.io", "api")))) return nil } diff --git a/cmd/timoni/mod_vet.go b/cmd/timoni/mod_vet.go index d53b8f47..db863f87 100644 --- a/cmd/timoni/mod_vet.go +++ b/cmd/timoni/mod_vet.go @@ -34,6 +34,7 @@ import ( "github.com/stefanprodan/timoni/internal/engine" "github.com/stefanprodan/timoni/internal/engine/fetcher" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" ) var vetModCmd = &cobra.Command{ @@ -81,7 +82,7 @@ func runVetModCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("module not found at path %s", vetModArgs.path) } - log := LoggerFrom(cmd.Context()) + log := logger.LoggerFrom(cmd.Context()) cuectx := cuecontext.New() tmpDir, err := os.MkdirTemp("", apiv1.FieldManager) @@ -179,7 +180,7 @@ func runVetModCmd(cmd *cobra.Command, args []string) error { for _, object := range objects { log.Info(fmt.Sprintf("%s %s", - colorizeSubject(ssautil.FmtUnstructured(object)), colorizeInfo("valid resource"))) + logger.ColorizeSubject(ssautil.FmtUnstructured(object)), logger.ColorizeInfo("valid resource"))) } images, err := builder.GetContainerImages(buildResult) @@ -195,15 +196,15 @@ func runVetModCmd(cmd *cobra.Command, args []string) error { if !strings.Contains(image, "@sha") { log.Info(fmt.Sprintf("%s %s", - colorizeSubject(image), colorizeWarning("valid image (digest missing)"))) + logger.ColorizeSubject(image), logger.ColorizeWarning("valid image (digest missing)"))) } else { log.Info(fmt.Sprintf("%s %s", - colorizeSubject(image), colorizeInfo("valid image"))) + logger.ColorizeSubject(image), logger.ColorizeInfo("valid image"))) } } log.Info(fmt.Sprintf("%s %s", - colorizeSubject(mod.Name), colorizeInfo("valid module"))) + logger.ColorizeSubject(mod.Name), logger.ColorizeInfo("valid module"))) return nil } diff --git a/cmd/timoni/runtime_build.go b/cmd/timoni/runtime_build.go index 5d6ae1f5..aaf98901 100644 --- a/cmd/timoni/runtime_build.go +++ b/cmd/timoni/runtime_build.go @@ -28,6 +28,7 @@ import ( apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" "github.com/stefanprodan/timoni/internal/engine" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/runtime" ) @@ -93,7 +94,7 @@ func runRuntimeBuildCmd(cmd *cobra.Command, args []string) error { } for _, cluster := range clusters { - log := LoggerRuntime(cmd.Context(), rt.Name, cluster.Name) + log := logger.LoggerRuntime(cmd.Context(), rt.Name, cluster.Name, true) kubeconfigArgs.Context = &cluster.KubeContext rm, err := runtime.NewResourceManager(kubeconfigArgs) @@ -116,7 +117,7 @@ func runRuntimeBuildCmd(cmd *cobra.Command, args []string) error { sort.Strings(keys) for _, k := range keys { - log.Info(fmt.Sprintf("%s: %s", colorizeSubject(k), values[k])) + log.Info(fmt.Sprintf("%s: %s", logger.ColorizeSubject(k), values[k])) } if len(values) == 0 { diff --git a/cmd/timoni/status.go b/cmd/timoni/status.go index 0e97cb15..3dd83107 100644 --- a/cmd/timoni/status.go +++ b/cmd/timoni/status.go @@ -26,6 +26,7 @@ import ( apierrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/runtime" apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" @@ -65,7 +66,7 @@ func runStatusCmd(cmd *cobra.Command, args []string) error { statusArgs.name = args[0] - log := LoggerInstance(cmd.Context(), statusArgs.name) + log := logger.LoggerInstance(cmd.Context(), statusArgs.name, true) rm, err := runtime.NewResourceManager(kubeconfigArgs) if err != nil { return err @@ -81,15 +82,15 @@ func runStatusCmd(cmd *cobra.Command, args []string) error { } log.Info(fmt.Sprintf("last applied %s", - colorizeSubject(instance.LastTransitionTime))) + logger.ColorizeSubject(instance.LastTransitionTime))) log.Info(fmt.Sprintf("module %s", - colorizeSubject(instance.Module.Repository+":"+instance.Module.Version))) + logger.ColorizeSubject(instance.Module.Repository+":"+instance.Module.Version))) log.Info(fmt.Sprintf("digest %s", - colorizeSubject(instance.Module.Digest))) + logger.ColorizeSubject(instance.Module.Digest))) for _, image := range instance.Images { log.Info(fmt.Sprintf("container image %s", - colorizeSubject(image))) + logger.ColorizeSubject(image))) } tm := runtime.InstanceManager{Instance: apiv1.Instance{Inventory: instance.Inventory}} @@ -103,19 +104,19 @@ func runStatusCmd(cmd *cobra.Command, args []string) error { err = rm.Client().Get(ctx, client.ObjectKeyFromObject(obj), obj) if err != nil { if apierrors.IsNotFound(err) { - log.Error(err, colorizeJoin(obj, errors.New("NotFound"))) + log.Error(err, logger.ColorizeJoin(obj, errors.New("NotFound"))) continue } - log.Error(err, colorizeJoin(obj, errors.New("Unknown"))) + log.Error(err, logger.ColorizeJoin(obj, errors.New("Unknown"))) continue } res, err := status.Compute(obj) if err != nil { - log.Error(err, colorizeJoin(obj, errors.New("Failed"))) + log.Error(err, logger.ColorizeJoin(obj, errors.New("Failed"))) continue } - log.Info(colorizeJoin(obj, res.Status, "-", res.Message)) + log.Info(logger.ColorizeJoin(obj, res.Status, "-", res.Message)) } return nil diff --git a/internal/apply/apply.go b/internal/apply/apply.go new file mode 100644 index 00000000..c57854af --- /dev/null +++ b/internal/apply/apply.go @@ -0,0 +1,245 @@ +/* +Copyright 2024 Stefan Prodan + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apply + +import ( + "context" + "errors" + "fmt" + "io" + "time" + + "cuelang.org/go/cue" + "github.com/fluxcd/pkg/ssa" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/cli-runtime/pkg/genericclioptions" + + apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" + "github.com/stefanprodan/timoni/internal/dyff" + "github.com/stefanprodan/timoni/internal/engine" + "github.com/stefanprodan/timoni/internal/logger" + "github.com/stefanprodan/timoni/internal/runtime" +) + +type Options struct { + Dir string + DryRun bool + Diff bool + Wait bool + Force bool + OverwriteOwnership bool + DiffOutput io.Writer + + KubeConfigFlags *genericclioptions.ConfigFlags + + ProgressStart func(string) ProgressStopper + OwnershipConflictHint string +} + +type ProgressStopper interface{ Stop() } + +type noopProgressStopper struct{} + +func (n *noopProgressStopper) Stop() {} + +func ApplyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleBuilder, buildResult cue.Value, instance *engine.BundleInstance, opts Options, timeout time.Duration) error { + isStandaloneInstance := instance.Bundle == "" + + if opts.DiffOutput == nil { + opts.DiffOutput = io.Discard + } + + if opts.ProgressStart == nil { + opts.ProgressStart = func(msg string) ProgressStopper { + log.Info(msg) + return &noopProgressStopper{} + } + } + + finalValues, err := builder.GetDefaultValues() + if err != nil { + return fmt.Errorf("failed to extract values: %w", err) + } + + sets, err := builder.GetApplySets(buildResult) + if err != nil { + return fmt.Errorf("failed to extract objects: %w", err) + } + + var objects []*unstructured.Unstructured + for _, set := range sets { + objects = append(objects, set.Objects...) + } + + rm, err := runtime.NewResourceManager(opts.KubeConfigFlags) + if err != nil { + return err + } + + rm.SetOwnerLabels(objects, instance.Name, instance.Namespace) + + exists := false + sm := runtime.NewStorageManager(rm) + storedInstance, err := sm.Get(ctx, instance.Name, instance.Namespace) + if err == nil { + exists = true + } + + nsExists, err := sm.NamespaceExists(ctx, instance.Namespace) + if err != nil { + return fmt.Errorf("instance init failed: %w", err) + } + + if !opts.OverwriteOwnership && exists && isStandaloneInstance { + if currentOwnerBundle := storedInstance.Labels[apiv1.BundleNameLabelKey]; currentOwnerBundle != "" { + return InstanceOwnershipConflictsErr(fmt.Sprintf("instance \"%s\" exists and is managed by bundle \"%s\"", instance.Name, currentOwnerBundle), "") + } + } + + im := runtime.NewInstanceManager(instance.Name, instance.Namespace, finalValues, instance.Module) + + if !isStandaloneInstance { + if im.Instance.Labels == nil { + im.Instance.Labels = make(map[string]string) + } + im.Instance.Labels[apiv1.BundleNameLabelKey] = instance.Bundle + } + + if err := im.AddObjects(objects); err != nil { + return fmt.Errorf("adding objects to instance failed: %w", err) + } + + staleObjects, err := sm.GetStaleObjects(ctx, &im.Instance) + if err != nil { + return fmt.Errorf("getting stale objects failed: %w", err) + } + + if opts.DryRun || opts.Diff { + if !nsExists { + log.Info(logger.ColorizeJoin(logger.ColorizeSubject("Namespace/"+instance.Namespace), + ssa.CreatedAction, logger.DryRunServer)) + } + if err := dyff.InstanceDryRunDiff( + logr.NewContext(ctx, log), + rm, + objects, + staleObjects, + nsExists, + opts.Dir, + opts.Diff, + opts.DiffOutput, + ); err != nil { + return err + } + + log.Info(logger.ColorizeJoin("applied successfully", logger.ColorizeDryRun("(server dry run)"))) + return nil + } + + if !exists { + log.Info(fmt.Sprintf("installing %s in namespace %s", + logger.ColorizeSubject(instance.Name), logger.ColorizeSubject(instance.Namespace))) + + if err := sm.Apply(ctx, &im.Instance, true); err != nil { + return fmt.Errorf("instance init failed: %w", err) + } + + if !nsExists { + log.Info(logger.ColorizeJoin(logger.ColorizeSubject("Namespace/"+instance.Namespace), ssa.CreatedAction)) + } + } else { + log.Info(fmt.Sprintf("upgrading %s in namespace %s", + logger.ColorizeSubject(instance.Name), logger.ColorizeSubject(instance.Namespace))) + } + + applyOpts := runtime.ApplyOptions(opts.Force, timeout) + applyOpts.WaitInterval = 5 * time.Second + + waitOptions := ssa.WaitOptions{ + Interval: applyOpts.WaitInterval, + Timeout: timeout, + FailFast: true, + } + + for _, set := range sets { + if len(sets) > 1 { + log.Info(fmt.Sprintf("applying %s", set.Name)) + } + + cs, err := rm.ApplyAllStaged(ctx, set.Objects, applyOpts) + if err != nil { + return err + } + for _, change := range cs.Entries { + log.Info(logger.ColorizeJoin(change)) + } + + if opts.Wait { + progress := opts.ProgressStart(fmt.Sprintf("waiting for %v resource(s) to become ready...", len(set.Objects))) + err = rm.Wait(set.Objects, waitOptions) + progress.Stop() + if err != nil { + return err + } + log.Info(fmt.Sprintf("%s resources %s", set.Name, logger.ColorizeReady("ready"))) + } + } + + if images, err := builder.GetContainerImages(buildResult); err == nil { + im.Instance.Images = images + } + + if err := sm.Apply(ctx, &im.Instance, true); err != nil { + return fmt.Errorf("storing instance failed: %w", err) + } + + var deletedObjects []*unstructured.Unstructured + if len(staleObjects) > 0 { + deleteOpts := runtime.DeleteOptions(instance.Name, instance.Namespace) + changeSet, err := rm.DeleteAll(ctx, staleObjects, deleteOpts) + if err != nil { + return fmt.Errorf("pruning objects failed: %w", err) + } + deletedObjects = runtime.SelectObjectsFromSet(changeSet, ssa.DeletedAction) + for _, change := range changeSet.Entries { + log.Info(logger.ColorizeJoin(change)) + } + } + + if opts.Wait { + if len(deletedObjects) > 0 { + progress := opts.ProgressStart(fmt.Sprintf("waiting for %v resource(s) to be finalized...", len(deletedObjects))) + err = rm.WaitForTermination(deletedObjects, waitOptions) + progress.Stop() + if err != nil { + return fmt.Errorf("waiting for termination failed: %w", err) + } + } + } + + return nil +} + +func InstanceOwnershipConflictsErr(description, hint string) error { + msg := "instance ownership conflict encountered." + if hint != "" { + msg += " " + hint + } + msg += " Conflict: " + description + return errors.New(msg) +} diff --git a/cmd/timoni/dyff.go b/internal/dyff/dyff.go similarity index 80% rename from cmd/timoni/dyff.go rename to internal/dyff/dyff.go index 77bd3ab3..d515e1f5 100644 --- a/cmd/timoni/dyff.go +++ b/internal/dyff/dyff.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Stefan Prodan +Copyright 2024 Stefan Prodan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package dyff import ( "context" @@ -33,6 +33,7 @@ import ( "sigs.k8s.io/yaml" apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" + "github.com/stefanprodan/timoni/internal/logger" ) // DyffPrinter is a printer that prints dyff reports. @@ -67,7 +68,7 @@ func (p *DyffPrinter) Print(w io.Writer, args ...interface{}) error { return nil } -func diffYAML(liveFile, mergedFile string, output io.Writer) error { +func DiffYAML(liveFile, mergedFile string, output io.Writer) error { from, to, err := ytbx.LoadFiles(liveFile, mergedFile) if err != nil { return fmt.Errorf("failed to load input files: %w", err) @@ -85,20 +86,21 @@ func diffYAML(liveFile, mergedFile string, output io.Writer) error { return printer.Print(output, report) } -func instanceDryRunDiff(ctx context.Context, +func InstanceDryRunDiff(ctx context.Context, rm *ssa.ResourceManager, objects []*unstructured.Unstructured, staleObjects []*unstructured.Unstructured, nsExists bool, tmpDir string, - withDiff bool) error { - log := LoggerFrom(ctx) + withDiff bool, + w io.Writer) error { + log := logger.LoggerFrom(ctx) diffOpts := ssa.DefaultDiffOptions() sort.Sort(ssa.SortableUnstructureds(objects)) for _, r := range objects { if !nsExists { - log.Info(colorizeJoin(r, ssa.CreatedAction, dryRunServer)) + log.Info(logger.ColorizeJoin(r, ssa.CreatedAction, logger.DryRunServer)) continue } @@ -108,18 +110,18 @@ func instanceDryRunDiff(ctx context.Context, if ssautil.AnyInMetadata(r, map[string]string{ apiv1.ForceAction: apiv1.EnabledValue, }) { - log.Info(colorizeJoin(r, ssa.CreatedAction, dryRunServer)) + log.Info(logger.ColorizeJoin(r, ssa.CreatedAction, logger.DryRunServer)) } else { - log.Error(nil, colorizeJoin(r, "immutable", dryRunServer)) + log.Error(nil, logger.ColorizeJoin(r, "immutable", logger.DryRunServer)) } } else { - log.Error(err, colorizeUnstructured(r)) + log.Error(err, logger.ColorizeUnstructured(r)) } continue } - log.Info(colorizeJoin(change, dryRunServer)) + log.Info(logger.ColorizeJoin(change, logger.DryRunServer)) if withDiff && change.Action == ssa.ConfiguredAction { liveYAML, _ := yaml.Marshal(liveObject) liveFile := filepath.Join(tmpDir, "live.yaml") @@ -133,14 +135,14 @@ func instanceDryRunDiff(ctx context.Context, return err } - if err := diffYAML(liveFile, mergedFile, rootCmd.OutOrStdout()); err != nil { + if err := DiffYAML(liveFile, mergedFile, w); err != nil { return err } } } for _, r := range staleObjects { - log.Info(colorizeJoin(r, ssa.DeletedAction, dryRunServer)) + log.Info(logger.ColorizeJoin(r, ssa.DeletedAction, logger.DryRunServer)) } return nil diff --git a/cmd/timoni/dyff_test.go b/internal/dyff/dyff_test.go similarity index 95% rename from cmd/timoni/dyff_test.go rename to internal/dyff/dyff_test.go index 6aa5ceed..afb62638 100644 --- a/cmd/timoni/dyff_test.go +++ b/internal/dyff/dyff_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package dyff import ( "bytes" @@ -42,7 +42,7 @@ func TestDiffYAML(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) buf := new(bytes.Buffer) - err = diffYAML(liveFile.Name(), mergedFile.Name(), buf) + err = DiffYAML(liveFile.Name(), mergedFile.Name(), buf) g.Expect(err).ToNot(HaveOccurred()) g.Expect(buf.String()).To(ContainSubstring("name: test-pod-merged")) } diff --git a/cmd/timoni/log.go b/internal/logger/logger.go similarity index 67% rename from cmd/timoni/log.go rename to internal/logger/logger.go index d130cd00..225b265f 100644 --- a/cmd/timoni/log.go +++ b/internal/logger/logger.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Stefan Prodan +Copyright 2024 Stefan Prodan Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package main +package logger import ( "context" @@ -39,12 +39,14 @@ import ( apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" ) +var logger logr.Logger + // NewConsoleLogger returns a human-friendly Logger. // Pretty print adds timestamp, log level and colorized output to the logs. -func NewConsoleLogger() logr.Logger { - color.NoColor = !rootArgs.coloredLog - zconfig := zerolog.ConsoleWriter{Out: color.Error, NoColor: !rootArgs.coloredLog} - if !rootArgs.prettyLog { +func NewConsoleLogger(colorize, prettify bool) logr.Logger { + color.NoColor = !colorize + zconfig := zerolog.ConsoleWriter{Out: color.Error, NoColor: !colorize} + if !prettify { zconfig.PartsExclude = []string{ zerolog.TimestampFieldName, zerolog.LevelFieldName, @@ -91,40 +93,40 @@ var ( } ) -type dryRunType string +type DryRunType string const ( - dryRunClient dryRunType = "(dry run)" - dryRunServer dryRunType = "(server dry run)" + DryRunClient DryRunType = "(dry run)" + DryRunServer DryRunType = "(server dry run)" ) -func colorizeJoin(values ...any) string { +func ColorizeJoin(values ...any) string { var sb strings.Builder for i, v := range values { if i > 0 { sb.WriteByte(' ') } - sb.WriteString(colorizeAny(v)) + sb.WriteString(ColorizeAny(v)) } return sb.String() } -func colorizeAny(v any) string { +func ColorizeAny(v any) string { switch v := v.(type) { case *unstructured.Unstructured: - return colorizeUnstructured(v) - case dryRunType: - return colorizeDryRun(v) + return ColorizeUnstructured(v) + case DryRunType: + return ColorizeDryRun(v) case ssa.Action: - return colorizeAction(v) + return ColorizeAction(v) case ssa.ChangeSetEntry: - return colorizeChangeSetEntry(v) + return ColorizeChangeSetEntry(v) case *ssa.ChangeSetEntry: - return colorizeChangeSetEntry(*v) + return ColorizeChangeSetEntry(*v) case status.Status: - return colorizeStatus(v) + return ColorizeStatus(v) case error: - return colorizeError(v) + return ColorizeError(v) case string: return v default: @@ -132,147 +134,146 @@ func colorizeAny(v any) string { } } -func colorizeSubject(subject string) string { +func ColorizeSubject(subject string) string { return color.CyanString(subject) } -func colorizeReady(subject string) string { +func ColorizeReady(subject string) string { return colorReady.Sprint(subject) } -func colorizeInfo(subject string) string { +func ColorizeInfo(subject string) string { return color.GreenString(subject) } -func colorizeWarning(subject string) string { +func ColorizeWarning(subject string) string { return color.YellowString(subject) } -func colorizeNamespaceFromArgs() string { - return colorizeSubject("Namespace/" + *kubeconfigArgs.Namespace) -} - -func colorizeUnstructured(object *unstructured.Unstructured) string { - return colorizeSubject(ssautil.FmtUnstructured(object)) +func ColorizeUnstructured(object *unstructured.Unstructured) string { + return ColorizeSubject(ssautil.FmtUnstructured(object)) } -func colorizeAction(action ssa.Action) string { +func ColorizeAction(action ssa.Action) string { if c, ok := colorPerAction[action]; ok { return c.Sprint(action) } return action.String() } -func colorizeChange(subject string, action ssa.Action) string { - return fmt.Sprintf("%s %s", colorizeSubject(subject), colorizeAction(action)) +func ColorizeChange(subject string, action ssa.Action) string { + return fmt.Sprintf("%s %s", ColorizeSubject(subject), ColorizeAction(action)) } -func colorizeChangeSetEntry(change ssa.ChangeSetEntry) string { - return colorizeChange(change.Subject, change.Action) +func ColorizeChangeSetEntry(change ssa.ChangeSetEntry) string { + return ColorizeChange(change.Subject, change.Action) } -func colorizeDryRun(dryRun dryRunType) string { +func ColorizeDryRun(dryRun DryRunType) string { return colorDryRun.Sprint(string(dryRun)) } -func colorizeError(err error) string { +func ColorizeError(err error) string { return colorError.Sprint(err.Error()) } -func colorizeStatus(status status.Status) string { +func ColorizeStatus(status status.Status) string { if c, ok := colorPerStatus[status]; ok { return c.Sprint(status) } return status.String() } -func colorizeBundle(bundle string) string { +func ColorizeBundle(bundle string) string { return colorCallerPrefix.Sprint("b:") + colorBundle.Sprint(bundle) } -func colorizeInstance(instance string) string { +func ColorizeInstance(instance string) string { return colorCallerPrefix.Sprint("i:") + colorInstance.Sprint(instance) } -func colorizeRuntime(runtime string) string { +func ColorizeRuntime(runtime string) string { return colorCallerPrefix.Sprint("r:") + colorInstance.Sprint(runtime) } -func colorizeCluster(cluster string) string { +func ColorizeCluster(cluster string) string { return colorCallerPrefix.Sprint("c:") + colorInstance.Sprint(cluster) } -func LoggerBundle(ctx context.Context, bundle, cluster string) logr.Logger { +func LoggerBundle(ctx context.Context, bundle, cluster string, prettify bool) logr.Logger { switch cluster { case apiv1.RuntimeDefaultName: - if !rootArgs.prettyLog { + if !prettify { return LoggerFrom(ctx, "bundle", bundle) } - return LoggerFrom(ctx, "caller", colorizeBundle(bundle)) + return LoggerFrom(ctx, "caller", ColorizeBundle(bundle)) default: - if !rootArgs.prettyLog { + if !prettify { return LoggerFrom(ctx, "bundle", bundle, "cluster", cluster) } return LoggerFrom(ctx, "caller", fmt.Sprintf("%s %s %s", - colorizeBundle(bundle), + ColorizeBundle(bundle), color.CyanString(">"), - colorizeCluster(cluster))) + ColorizeCluster(cluster))) } } -func LoggerInstance(ctx context.Context, instance string) logr.Logger { - if !rootArgs.prettyLog { +func LoggerInstance(ctx context.Context, instance string, prettify bool) logr.Logger { + if !prettify { return LoggerFrom(ctx, "instance", instance) } - return LoggerFrom(ctx, "caller", colorizeInstance(instance)) + return LoggerFrom(ctx, "caller", ColorizeInstance(instance)) } -func LoggerBundleInstance(ctx context.Context, bundle, cluster, instance string) logr.Logger { +func LoggerBundleInstance(ctx context.Context, bundle, cluster, instance string, prettify bool) logr.Logger { switch cluster { case apiv1.RuntimeDefaultName: - if !rootArgs.prettyLog { + if !prettify { return LoggerFrom(ctx, "bundle", bundle, "instance", instance) } return LoggerFrom(ctx, "caller", fmt.Sprintf("%s %s %s", - colorizeBundle(bundle), + ColorizeBundle(bundle), color.CyanString(">"), - colorizeInstance(instance))) + ColorizeInstance(instance))) default: - if !rootArgs.prettyLog { + if !prettify { return LoggerFrom(ctx, "bundle", bundle, "cluster", cluster, "instance", instance) } return LoggerFrom(ctx, "caller", fmt.Sprintf("%s %s %s %s %s", - colorizeBundle(bundle), + ColorizeBundle(bundle), color.CyanString(">"), - colorizeCluster(cluster), + ColorizeCluster(cluster), color.CyanString(">"), - colorizeInstance(instance))) + ColorizeInstance(instance))) } } -func LoggerRuntime(ctx context.Context, runtime, cluster string) logr.Logger { +func LoggerRuntime(ctx context.Context, runtime, cluster string, prettify bool) logr.Logger { switch cluster { case apiv1.RuntimeDefaultName: - if !rootArgs.prettyLog { + if !prettify { return LoggerFrom(ctx, "runtime", runtime) } - return LoggerFrom(ctx, "caller", colorizeRuntime(runtime)) + return LoggerFrom(ctx, "caller", ColorizeRuntime(runtime)) default: - if !rootArgs.prettyLog { + if !prettify { return LoggerFrom(ctx, "runtime", runtime, "cluster", cluster) } return LoggerFrom(ctx, "caller", - fmt.Sprintf("%s %s %s", colorizeRuntime(runtime), - color.CyanString(">"), colorizeCluster(cluster))) + fmt.Sprintf("%s %s %s", ColorizeRuntime(runtime), + color.CyanString(">"), ColorizeCluster(cluster))) } } // LoggerFrom returns a logr.Logger with predefined values from a context.Context. func LoggerFrom(ctx context.Context, keysAndValues ...interface{}) logr.Logger { + if logger.IsZero() { + logger = NewConsoleLogger(false, false) + } newLogger := logger if ctx != nil { if l, err := logr.FromContext(ctx); err == nil { From e6526f7fbaf064e6cf74a31a9597cebbcb73c04e Mon Sep 17 00:00:00 2001 From: Ilya Dmitrichenko Date: Wed, 13 Mar 2024 21:55:27 +0000 Subject: [PATCH 06/10] Undo moving of some logger function reliant on shared state Signed-off-by: Ilya Dmitrichenko --- cmd/timoni/apply.go | 3 +- cmd/timoni/artifact_pull.go | 2 +- cmd/timoni/artifact_push.go | 2 +- cmd/timoni/artifact_tag.go | 2 +- cmd/timoni/bundle_apply.go | 4 +- cmd/timoni/bundle_delete.go | 4 +- cmd/timoni/bundle_status.go | 4 +- cmd/timoni/bundle_vet.go | 7 +-- cmd/timoni/delete.go | 2 +- cmd/timoni/logger.go | 111 +++++++++++++++++++++++++++++++++++ cmd/timoni/mod_init.go | 2 +- cmd/timoni/mod_pull.go | 2 +- cmd/timoni/mod_push.go | 2 +- cmd/timoni/mod_vendor_crd.go | 2 +- cmd/timoni/mod_vendor_k8s.go | 2 +- cmd/timoni/mod_vet.go | 2 +- cmd/timoni/runtime_build.go | 2 +- cmd/timoni/status.go | 2 +- internal/dyff/dyff.go | 3 +- internal/logger/logger.go | 88 --------------------------- 20 files changed, 135 insertions(+), 113 deletions(-) create mode 100644 cmd/timoni/logger.go diff --git a/cmd/timoni/apply.go b/cmd/timoni/apply.go index fe52ba69..c282c587 100644 --- a/cmd/timoni/apply.go +++ b/cmd/timoni/apply.go @@ -31,7 +31,6 @@ import ( "github.com/stefanprodan/timoni/internal/engine" "github.com/stefanprodan/timoni/internal/engine/fetcher" "github.com/stefanprodan/timoni/internal/flags" - "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/runtime" ) @@ -142,7 +141,7 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { applyArgs.name = args[0] applyArgs.module = args[1] - log := logger.LoggerInstance(cmd.Context(), applyArgs.name, true) + log := loggerInstance(cmd.Context(), applyArgs.name, true) version := applyArgs.version.String() if version == "" { diff --git a/cmd/timoni/artifact_pull.go b/cmd/timoni/artifact_pull.go index f7e16923..78bf81d9 100644 --- a/cmd/timoni/artifact_pull.go +++ b/cmd/timoni/artifact_pull.go @@ -106,7 +106,7 @@ func pullArtifactCmdRun(cmd *cobra.Command, args []string) error { } ociURL := args[0] - log := logger.LoggerFrom(cmd.Context()) + log := LoggerFrom(cmd.Context()) if err := os.MkdirAll(pullArtifactArgs.output, os.ModePerm); err != nil { return fmt.Errorf("invalid output path %s: %w", pullArtifactArgs.output, err) diff --git a/cmd/timoni/artifact_push.go b/cmd/timoni/artifact_push.go index ca7376e4..bc67c3d8 100644 --- a/cmd/timoni/artifact_push.go +++ b/cmd/timoni/artifact_push.go @@ -119,7 +119,7 @@ func pushArtifactCmdRun(cmd *cobra.Command, args []string) error { pushArtifactArgs.ignorePaths = append(pushArtifactArgs.ignorePaths, ps...) } - log := logger.LoggerFrom(cmd.Context()) + log := LoggerFrom(cmd.Context()) ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() diff --git a/cmd/timoni/artifact_tag.go b/cmd/timoni/artifact_tag.go index 24067a79..5feb19cd 100644 --- a/cmd/timoni/artifact_tag.go +++ b/cmd/timoni/artifact_tag.go @@ -66,7 +66,7 @@ func tagArtifactCmdRun(cmd *cobra.Command, args []string) error { spin := logger.StartSpinner("tagging artifact") defer spin.Stop() - log := logger.LoggerFrom(cmd.Context()) + log := LoggerFrom(cmd.Context()) ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) defer cancel() diff --git a/cmd/timoni/bundle_apply.go b/cmd/timoni/bundle_apply.go index e1913d7e..f5f5873d 100644 --- a/cmd/timoni/bundle_apply.go +++ b/cmd/timoni/bundle_apply.go @@ -191,7 +191,7 @@ func runBundleApplyCmd(cmd *cobra.Command, _ []string) error { return err } - log := logger.LoggerBundle(cmd.Context(), bundle.Name, cluster.Name, true) + log := loggerBundle(cmd.Context(), bundle.Name, cluster.Name, true) if !bundleApplyArgs.overwriteOwnership { err = bundleInstancesOwnershipConflicts(bundle.Instances) @@ -281,7 +281,7 @@ func fetchBundleInstanceModule(ctx context.Context, instance *engine.BundleInsta } func applyBundleInstance(ctx context.Context, cuectx *cue.Context, instance *engine.BundleInstance, kubeVersion string, rootDir string, diffOutput io.Writer) error { - log := logger.LoggerBundleInstance(ctx, instance.Bundle, instance.Cluster, instance.Name, true) + log := loggerBundleInstance(ctx, instance.Bundle, instance.Cluster, instance.Name, true) modDir := path.Join(rootDir, instance.Name, "module") builder := engine.NewModuleBuilder( diff --git a/cmd/timoni/bundle_delete.go b/cmd/timoni/bundle_delete.go index 8160271a..58986d49 100644 --- a/cmd/timoni/bundle_delete.go +++ b/cmd/timoni/bundle_delete.go @@ -121,7 +121,7 @@ func runBundleDelCmd(cmd *cobra.Command, args []string) error { return err } - log := logger.LoggerBundle(ctx, bundleDelArgs.name, cluster.Name, true) + log := loggerBundle(ctx, bundleDelArgs.name, cluster.Name, true) if len(instances) == 0 { log.Error(nil, "no instances found in bundle") @@ -147,7 +147,7 @@ func runBundleDelCmd(cmd *cobra.Command, args []string) error { } func deleteBundleInstance(ctx context.Context, instance *engine.BundleInstance, wait bool, dryrun bool) error { - log := logger.LoggerBundle(ctx, instance.Bundle, instance.Cluster, true) + log := loggerBundle(ctx, instance.Bundle, instance.Cluster, true) sm, err := runtime.NewResourceManager(kubeconfigArgs) if err != nil { diff --git a/cmd/timoni/bundle_status.go b/cmd/timoni/bundle_status.go index b423c75e..6d6eb8db 100644 --- a/cmd/timoni/bundle_status.go +++ b/cmd/timoni/bundle_status.go @@ -104,7 +104,7 @@ func runBundleStatusCmd(cmd *cobra.Command, args []string) error { return err } - log := logger.LoggerBundle(ctx, bundleStatusArgs.name, cluster.Name, true) + log := loggerBundle(ctx, bundleStatusArgs.name, cluster.Name, true) if len(instances) == 0 { log.Error(nil, "no instances found in bundle") @@ -113,7 +113,7 @@ func runBundleStatusCmd(cmd *cobra.Command, args []string) error { } for _, instance := range instances { - log := logger.LoggerBundleInstance(ctx, bundleStatusArgs.name, cluster.Name, instance.Name, true) + log := loggerBundleInstance(ctx, bundleStatusArgs.name, cluster.Name, instance.Name, true) log.Info(fmt.Sprintf("last applied %s", logger.ColorizeSubject(instance.LastTransitionTime))) diff --git a/cmd/timoni/bundle_vet.go b/cmd/timoni/bundle_vet.go index f36d5345..15acb445 100644 --- a/cmd/timoni/bundle_vet.go +++ b/cmd/timoni/bundle_vet.go @@ -31,7 +31,6 @@ import ( apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" "github.com/stefanprodan/timoni/internal/engine" "github.com/stefanprodan/timoni/internal/flags" - "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/runtime" ) @@ -79,7 +78,7 @@ func init() { } func runBundleVetCmd(cmd *cobra.Command, args []string) error { - log := logger.LoggerFrom(cmd.Context()) + log := LoggerFrom(cmd.Context()) files := bundleVetArgs.files if len(files) == 0 { return fmt.Errorf("no bundle provided with -f") @@ -170,7 +169,7 @@ func runBundleVetCmd(cmd *cobra.Command, args []string) error { return err } - log = logger.LoggerBundle(logr.NewContext(cmd.Context(), log), bundle.Name, apiv1.RuntimeDefaultName, true) + log = loggerBundle(logr.NewContext(cmd.Context(), log), bundle.Name, apiv1.RuntimeDefaultName, true) if len(bundle.Instances) == 0 { return fmt.Errorf("no instances found in bundle") @@ -194,7 +193,7 @@ func runBundleVetCmd(cmd *cobra.Command, args []string) error { if i.Namespace == "" { return fmt.Errorf("instance %s does not have a namespace", i.Name) } - log := logger.LoggerBundleInstance(logr.NewContext(cmd.Context(), log), bundle.Name, cluster.Name, i.Name, true) + log := loggerBundleInstance(logr.NewContext(cmd.Context(), log), bundle.Name, cluster.Name, i.Name, true) log.Info("instance is valid") } } diff --git a/cmd/timoni/delete.go b/cmd/timoni/delete.go index 5262dc7e..a5b68f6c 100644 --- a/cmd/timoni/delete.go +++ b/cmd/timoni/delete.go @@ -73,7 +73,7 @@ func runDeleteCmd(cmd *cobra.Command, args []string) error { deleteArgs.name = args[0] - log := logger.LoggerInstance(cmd.Context(), deleteArgs.name, true) + log := loggerInstance(cmd.Context(), deleteArgs.name, true) sm, err := runtime.NewResourceManager(kubeconfigArgs) if err != nil { return err diff --git a/cmd/timoni/logger.go b/cmd/timoni/logger.go new file mode 100644 index 00000000..b3ea4aa5 --- /dev/null +++ b/cmd/timoni/logger.go @@ -0,0 +1,111 @@ +/* +Copyright 2024 Stefan Prodan + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "fmt" + + "github.com/fatih/color" + "github.com/go-logr/logr" + + apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" + "github.com/stefanprodan/timoni/internal/logger" +) + +func loggerBundle(ctx context.Context, bundle, cluster string, prettify bool) logr.Logger { + switch cluster { + case apiv1.RuntimeDefaultName: + if !prettify { + return LoggerFrom(ctx, "bundle", bundle) + } + return LoggerFrom(ctx, "caller", logger.ColorizeBundle(bundle)) + default: + if !prettify { + return LoggerFrom(ctx, "bundle", bundle, "cluster", cluster) + } + return LoggerFrom(ctx, "caller", + fmt.Sprintf("%s %s %s", + logger.ColorizeBundle(bundle), + color.CyanString(">"), + logger.ColorizeCluster(cluster))) + } +} + +func loggerInstance(ctx context.Context, instance string, prettify bool) logr.Logger { + if !prettify { + return LoggerFrom(ctx, "instance", instance) + } + return LoggerFrom(ctx, "caller", logger.ColorizeInstance(instance)) +} + +func loggerBundleInstance(ctx context.Context, bundle, cluster, instance string, prettify bool) logr.Logger { + switch cluster { + case apiv1.RuntimeDefaultName: + if !prettify { + return LoggerFrom(ctx, "bundle", bundle, "instance", instance) + } + return LoggerFrom(ctx, "caller", + fmt.Sprintf("%s %s %s", + logger.ColorizeBundle(bundle), + color.CyanString(">"), + logger.ColorizeInstance(instance))) + default: + if !prettify { + return LoggerFrom(ctx, "bundle", bundle, "cluster", cluster, "instance", instance) + } + return LoggerFrom(ctx, "caller", + fmt.Sprintf("%s %s %s %s %s", + logger.ColorizeBundle(bundle), + color.CyanString(">"), + logger.ColorizeCluster(cluster), + color.CyanString(">"), + logger.ColorizeInstance(instance))) + + } +} + +func loggerRuntime(ctx context.Context, runtime, cluster string, prettify bool) logr.Logger { + switch cluster { + case apiv1.RuntimeDefaultName: + if !prettify { + return LoggerFrom(ctx, "runtime", runtime) + } + return LoggerFrom(ctx, "caller", logger.ColorizeRuntime(runtime)) + default: + if !prettify { + return LoggerFrom(ctx, "runtime", runtime, "cluster", cluster) + } + return LoggerFrom(ctx, "caller", + fmt.Sprintf("%s %s %s", logger.ColorizeRuntime(runtime), + color.CyanString(">"), logger.ColorizeCluster(cluster))) + } +} + +// LoggerFrom returns a logr.Logger with predefined values from a context.Context. +func LoggerFrom(ctx context.Context, keysAndValues ...interface{}) logr.Logger { + if cliLogger.IsZero() { + cliLogger = logger.NewConsoleLogger(false, false) + } + newLogger := cliLogger + if ctx != nil { + if l, err := logr.FromContext(ctx); err == nil { + newLogger = l + } + } + return newLogger.WithValues(keysAndValues...) +} diff --git a/cmd/timoni/mod_init.go b/cmd/timoni/mod_init.go index 2686ae4c..4f2b1b3c 100644 --- a/cmd/timoni/mod_init.go +++ b/cmd/timoni/mod_init.go @@ -77,7 +77,7 @@ func runInitModCmd(cmd *cobra.Command, args []string) error { initModArgs.path = "." } - log := logger.LoggerFrom(cmd.Context()) + log := LoggerFrom(cmd.Context()) if fs, err := os.Stat(initModArgs.path); err != nil || !fs.IsDir() { return fmt.Errorf("path not found: %s", initModArgs.path) diff --git a/cmd/timoni/mod_pull.go b/cmd/timoni/mod_pull.go index bdf4185d..9da09c5e 100644 --- a/cmd/timoni/mod_pull.go +++ b/cmd/timoni/mod_pull.go @@ -123,7 +123,7 @@ func pullCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("invalid output path %s: %w", pullModArgs.output, err) } - log := logger.LoggerFrom(cmd.Context()) + log := LoggerFrom(cmd.Context()) if pullModArgs.verify != "" { err := oci.VerifyArtifact(log, diff --git a/cmd/timoni/mod_push.go b/cmd/timoni/mod_push.go index 89ce1a7d..5ef237d2 100644 --- a/cmd/timoni/mod_push.go +++ b/cmd/timoni/mod_push.go @@ -124,7 +124,7 @@ func pushModCmdRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("module not found at path %s", pushModArgs.module) } - log := logger.LoggerFrom(cmd.Context()) + log := LoggerFrom(cmd.Context()) annotations, err := oci.ParseAnnotations(pushModArgs.annotations) if err != nil { diff --git a/cmd/timoni/mod_vendor_crd.go b/cmd/timoni/mod_vendor_crd.go index 5e1b788d..c60bdbf4 100644 --- a/cmd/timoni/mod_vendor_crd.go +++ b/cmd/timoni/mod_vendor_crd.go @@ -73,7 +73,7 @@ func runVendorCrdCmd(cmd *cobra.Command, args []string) error { vendorCrdArgs.modRoot = args[0] } - log := logger.LoggerFrom(cmd.Context()) + log := LoggerFrom(cmd.Context()) cuectx := cuecontext.New() // Make sure we're importing into a CUE module. diff --git a/cmd/timoni/mod_vendor_k8s.go b/cmd/timoni/mod_vendor_k8s.go index 2b4fc4ca..ac369ad0 100644 --- a/cmd/timoni/mod_vendor_k8s.go +++ b/cmd/timoni/mod_vendor_k8s.go @@ -63,7 +63,7 @@ func runVendorK8sCmd(cmd *cobra.Command, args []string) error { vendorK8sArgs.modRoot = args[0] } - log := logger.LoggerFrom(cmd.Context()) + log := LoggerFrom(cmd.Context()) // Make sure we're importing into a CUE module. cueModDir := path.Join(vendorK8sArgs.modRoot, "cue.mod") diff --git a/cmd/timoni/mod_vet.go b/cmd/timoni/mod_vet.go index db863f87..04fb240e 100644 --- a/cmd/timoni/mod_vet.go +++ b/cmd/timoni/mod_vet.go @@ -82,7 +82,7 @@ func runVetModCmd(cmd *cobra.Command, args []string) error { return fmt.Errorf("module not found at path %s", vetModArgs.path) } - log := logger.LoggerFrom(cmd.Context()) + log := LoggerFrom(cmd.Context()) cuectx := cuecontext.New() tmpDir, err := os.MkdirTemp("", apiv1.FieldManager) diff --git a/cmd/timoni/runtime_build.go b/cmd/timoni/runtime_build.go index aaf98901..86810c7e 100644 --- a/cmd/timoni/runtime_build.go +++ b/cmd/timoni/runtime_build.go @@ -94,7 +94,7 @@ func runRuntimeBuildCmd(cmd *cobra.Command, args []string) error { } for _, cluster := range clusters { - log := logger.LoggerRuntime(cmd.Context(), rt.Name, cluster.Name, true) + log := loggerRuntime(cmd.Context(), rt.Name, cluster.Name, true) kubeconfigArgs.Context = &cluster.KubeContext rm, err := runtime.NewResourceManager(kubeconfigArgs) diff --git a/cmd/timoni/status.go b/cmd/timoni/status.go index 3dd83107..de61227c 100644 --- a/cmd/timoni/status.go +++ b/cmd/timoni/status.go @@ -66,7 +66,7 @@ func runStatusCmd(cmd *cobra.Command, args []string) error { statusArgs.name = args[0] - log := logger.LoggerInstance(cmd.Context(), statusArgs.name, true) + log := loggerInstance(cmd.Context(), statusArgs.name, true) rm, err := runtime.NewResourceManager(kubeconfigArgs) if err != nil { return err diff --git a/internal/dyff/dyff.go b/internal/dyff/dyff.go index d515e1f5..701b0ab7 100644 --- a/internal/dyff/dyff.go +++ b/internal/dyff/dyff.go @@ -27,6 +27,7 @@ import ( "github.com/fluxcd/pkg/ssa" ssaerr "github.com/fluxcd/pkg/ssa/errors" ssautil "github.com/fluxcd/pkg/ssa/utils" + "github.com/go-logr/logr" "github.com/gonvenience/ytbx" "github.com/homeport/dyff/pkg/dyff" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -94,7 +95,7 @@ func InstanceDryRunDiff(ctx context.Context, tmpDir string, withDiff bool, w io.Writer) error { - log := logger.LoggerFrom(ctx) + log := logr.FromContextOrDiscard(ctx) diffOpts := ssa.DefaultDiffOptions() sort.Sort(ssa.SortableUnstructureds(objects)) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 225b265f..0db25df4 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -17,7 +17,6 @@ limitations under the License. package logger import ( - "context" "fmt" "io" "os" @@ -35,12 +34,8 @@ import ( "github.com/rs/zerolog" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" runtimeLog "sigs.k8s.io/controller-runtime/pkg/log" - - apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" ) -var logger logr.Logger - // NewConsoleLogger returns a human-friendly Logger. // Pretty print adds timestamp, log level and colorized output to the logs. func NewConsoleLogger(colorize, prettify bool) logr.Logger { @@ -200,89 +195,6 @@ func ColorizeCluster(cluster string) string { return colorCallerPrefix.Sprint("c:") + colorInstance.Sprint(cluster) } -func LoggerBundle(ctx context.Context, bundle, cluster string, prettify bool) logr.Logger { - switch cluster { - case apiv1.RuntimeDefaultName: - if !prettify { - return LoggerFrom(ctx, "bundle", bundle) - } - return LoggerFrom(ctx, "caller", ColorizeBundle(bundle)) - default: - if !prettify { - return LoggerFrom(ctx, "bundle", bundle, "cluster", cluster) - } - return LoggerFrom(ctx, "caller", - fmt.Sprintf("%s %s %s", - ColorizeBundle(bundle), - color.CyanString(">"), - ColorizeCluster(cluster))) - } -} - -func LoggerInstance(ctx context.Context, instance string, prettify bool) logr.Logger { - if !prettify { - return LoggerFrom(ctx, "instance", instance) - } - return LoggerFrom(ctx, "caller", ColorizeInstance(instance)) -} - -func LoggerBundleInstance(ctx context.Context, bundle, cluster, instance string, prettify bool) logr.Logger { - switch cluster { - case apiv1.RuntimeDefaultName: - if !prettify { - return LoggerFrom(ctx, "bundle", bundle, "instance", instance) - } - return LoggerFrom(ctx, "caller", - fmt.Sprintf("%s %s %s", - ColorizeBundle(bundle), - color.CyanString(">"), - ColorizeInstance(instance))) - default: - if !prettify { - return LoggerFrom(ctx, "bundle", bundle, "cluster", cluster, "instance", instance) - } - return LoggerFrom(ctx, "caller", - fmt.Sprintf("%s %s %s %s %s", - ColorizeBundle(bundle), - color.CyanString(">"), - ColorizeCluster(cluster), - color.CyanString(">"), - ColorizeInstance(instance))) - - } -} - -func LoggerRuntime(ctx context.Context, runtime, cluster string, prettify bool) logr.Logger { - switch cluster { - case apiv1.RuntimeDefaultName: - if !prettify { - return LoggerFrom(ctx, "runtime", runtime) - } - return LoggerFrom(ctx, "caller", ColorizeRuntime(runtime)) - default: - if !prettify { - return LoggerFrom(ctx, "runtime", runtime, "cluster", cluster) - } - return LoggerFrom(ctx, "caller", - fmt.Sprintf("%s %s %s", ColorizeRuntime(runtime), - color.CyanString(">"), ColorizeCluster(cluster))) - } -} - -// LoggerFrom returns a logr.Logger with predefined values from a context.Context. -func LoggerFrom(ctx context.Context, keysAndValues ...interface{}) logr.Logger { - if logger.IsZero() { - logger = NewConsoleLogger(false, false) - } - newLogger := logger - if ctx != nil { - if l, err := logr.FromContext(ctx); err == nil { - newLogger = l - } - } - return newLogger.WithValues(keysAndValues...) -} - // StartSpinner starts a spinner with the given message. func StartSpinner(msg string) *spinner.Spinner { s := spinner.New(spinner.CharSets[11], 100*time.Millisecond, spinner.WithWriter(os.Stderr)) From 90a26a3cb260dfb3d48072d97135e8cb9ea7894f Mon Sep 17 00:00:00 2001 From: Ilya Dmitrichenko Date: Thu, 14 Mar 2024 10:21:33 +0000 Subject: [PATCH 07/10] Split up applier logic into multiple functions Provide an interactive and non-interactive version that have different logging, ensure reasonanble level of reuse within each implementation Signed-off-by: Ilya Dmitrichenko --- cmd/timoni/apply.go | 40 ++-- cmd/timoni/bundle_apply.go | 59 +++--- internal/apply/apply.go | 373 ++++++++++++++++++++++++++----------- 3 files changed, 327 insertions(+), 145 deletions(-) diff --git a/cmd/timoni/apply.go b/cmd/timoni/apply.go index c282c587..39e41991 100644 --- a/cmd/timoni/apply.go +++ b/cmd/timoni/apply.go @@ -112,8 +112,6 @@ type applyFlags struct { var applyArgs applyFlags -const ownershipConflictHint = "Apply with \"--overwrite-ownership\" to gain instance ownership." - func init() { applyCmd.Flags().VarP(&applyArgs.version, applyArgs.version.Type(), applyArgs.version.Shorthand(), applyArgs.version.Description()) applyCmd.Flags().VarP(&applyArgs.pkg, applyArgs.pkg.Type(), applyArgs.pkg.Shorthand(), applyArgs.pkg.Description()) @@ -226,25 +224,33 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { ctx, cancel := context.WithTimeout(cmd.Context(), rootArgs.timeout) defer cancel() - opts := apply.Options{ - Dir: tmpDir, - DryRun: applyArgs.dryrun, - Diff: applyArgs.diff, - Wait: applyArgs.wait, - Force: applyArgs.force, - OverwriteOwnership: applyArgs.overwriteOwnership, - DiffOutput: cmd.OutOrStdout(), - KubeConfigFlags: kubeconfigArgs, - OwnershipConflictHint: ownershipConflictHint, - // ProgressStart: logger.StartSpinner, - } - - bi := &engine.BundleInstance{ + instance := &engine.BundleInstance{ Name: applyArgs.name, Namespace: *kubeconfigArgs.Namespace, Module: *mod, Bundle: "", } - return apply.ApplyInstance(ctx, log, builder, buildResult, bi, opts, rootArgs.timeout) + applier := apply.NewInstanceApplier(log, + &apply.CommonOptions{ + Dir: tmpDir, + Wait: applyArgs.wait, + Force: applyArgs.force, + OverwriteOwnership: applyArgs.overwriteOwnership, + }, + rootArgs.timeout, + ) + if err := applier.Init(ctx, builder, buildResult, instance, kubeconfigArgs); err != nil { + return annotateInstanceOwnershipConflictErr(err) + } + return applier.ApplyInstanceInteractively(ctx, log, + builder, + buildResult, + apply.InteractiveOptions{ + DryRun: applyArgs.dryrun, + Diff: applyArgs.diff, + DiffOutput: cmd.OutOrStdout(), + // ProgressStart: logger.StartSpinner, + }, + ) } diff --git a/cmd/timoni/bundle_apply.go b/cmd/timoni/bundle_apply.go index f5f5873d..c0676544 100644 --- a/cmd/timoni/bundle_apply.go +++ b/cmd/timoni/bundle_apply.go @@ -24,7 +24,6 @@ import ( "maps" "os" "path" - "strings" "time" "cuelang.org/go/cue" @@ -196,7 +195,7 @@ func runBundleApplyCmd(cmd *cobra.Command, _ []string) error { if !bundleApplyArgs.overwriteOwnership { err = bundleInstancesOwnershipConflicts(bundle.Instances) if err != nil { - return err + return annotateInstanceOwnershipConflictErr(err) } } @@ -316,20 +315,37 @@ func applyBundleInstance(ctx context.Context, cuectx *cue.Context, instance *eng return describeErr(modDir, "build failed for "+instance.Name, err) } - opts := apply.Options{ - Dir: rootDir, - DryRun: bundleApplyArgs.dryrun, - Diff: bundleApplyArgs.diff, - Wait: bundleApplyArgs.wait, - Force: bundleApplyArgs.force, - OverwriteOwnership: bundleApplyArgs.overwriteOwnership, - DiffOutput: diffOutput, - KubeConfigFlags: kubeconfigArgs, - OwnershipConflictHint: ownershipConflictHint, - // ProgressStart: logger.StartSpinner, + applier := apply.NewInstanceApplier(log, + &apply.CommonOptions{ + Dir: rootDir, + Wait: bundleApplyArgs.wait, + Force: bundleApplyArgs.force, + OverwriteOwnership: bundleApplyArgs.overwriteOwnership, + }, + rootArgs.timeout, + ) + + if err := applier.Init(ctx, builder, buildResult, instance, kubeconfigArgs); err != nil { + return annotateInstanceOwnershipConflictErr(err) } - return apply.ApplyInstance(ctx, log, builder, buildResult, instance, opts, rootArgs.timeout) + return applier.ApplyInstanceInteractively(ctx, log, + builder, + buildResult, + apply.InteractiveOptions{ + DryRun: bundleApplyArgs.dryrun, + Diff: bundleApplyArgs.diff, + DiffOutput: diffOutput, + // ProgressStart: logger.StartSpinner, + }, + ) +} + +func annotateInstanceOwnershipConflictErr(err error) error { + if errors.Is(err, &apply.InstanceOwnershipConflictErr{}) { + return fmt.Errorf("%s %s", err, "Apply with \"--overwrite-ownership\" to gain instance ownership.") + } + return err } func saveReaderToFile(reader io.Reader) (string, error) { @@ -348,7 +364,7 @@ func saveReaderToFile(reader io.Reader) (string, error) { } func bundleInstancesOwnershipConflicts(bundleInstances []*engine.BundleInstance) error { - var conflicts []string + var conflicts apply.InstanceOwnershipConflictErr rm, err := runtime.NewResourceManager(kubeconfigArgs) if err != nil { return err @@ -361,16 +377,17 @@ func bundleInstancesOwnershipConflicts(bundleInstances []*engine.BundleInstance) for _, instance := range bundleInstances { if existingInstance, err := sm.Get(ctx, instance.Name, instance.Namespace); err == nil { currentOwnerBundle := existingInstance.Labels[apiv1.BundleNameLabelKey] - if currentOwnerBundle == "" { - conflicts = append(conflicts, fmt.Sprintf("instance \"%s\" exists and is managed by no bundle", instance.Name)) - } else if currentOwnerBundle != instance.Bundle { - conflicts = append(conflicts, fmt.Sprintf("instance \"%s\" exists and is managed by another bundle \"%s\"", instance.Name, currentOwnerBundle)) + if currentOwnerBundle == "" || currentOwnerBundle != instance.Bundle { + conflicts = append(conflicts, apply.InstanceOwnershipConflict{ + InstanceName: instance.Name, + CurrentOwnerBundle: currentOwnerBundle, + }) } } } + if len(conflicts) > 0 { - return apply.InstanceOwnershipConflictsErr(strings.Join(conflicts, "; "), ownershipConflictHint) + return &conflicts } - return nil } diff --git a/internal/apply/apply.go b/internal/apply/apply.go index c57854af..15436e1d 100644 --- a/internal/apply/apply.go +++ b/internal/apply/apply.go @@ -18,15 +18,16 @@ package apply import ( "context" - "errors" "fmt" "io" + "strings" "time" "cuelang.org/go/cue" "github.com/fluxcd/pkg/ssa" "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + kerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/cli-runtime/pkg/genericclioptions" apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" @@ -36,114 +37,165 @@ import ( "github.com/stefanprodan/timoni/internal/runtime" ) -type Options struct { +type CommonOptions struct { Dir string - DryRun bool - Diff bool Wait bool Force bool OverwriteOwnership bool - DiffOutput io.Writer +} - KubeConfigFlags *genericclioptions.ConfigFlags +type InteractiveOptions struct { + DryRun bool + Diff bool + DiffOutput io.Writer - ProgressStart func(string) ProgressStopper - OwnershipConflictHint string + ProgressStart ProgressStarter } -type ProgressStopper interface{ Stop() } +type InstanceApplier struct { + opts *CommonOptions -type noopProgressStopper struct{} + instanceExists bool -func (n *noopProgressStopper) Stop() {} + sets []engine.ResourceSet -func ApplyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleBuilder, buildResult cue.Value, instance *engine.BundleInstance, opts Options, timeout time.Duration) error { - isStandaloneInstance := instance.Bundle == "" + currentObjects, staleObjects []*unstructured.Unstructured - if opts.DiffOutput == nil { - opts.DiffOutput = io.Discard - } + storageManager *runtime.StorageManager + instanceManager *runtime.InstanceManager + resourceManager *ssa.ResourceManager + + applyOptions ssa.ApplyOptions + waitOptions ssa.WaitOptions + + progressStart ProgressStarter +} + +type ( + ProgressStarter func(string) ProgressStopper + ProgressStopper interface{ Stop() } +) + +type noopProgressStopper struct{} - if opts.ProgressStart == nil { - opts.ProgressStart = func(msg string) ProgressStopper { +func (n *noopProgressStopper) Stop() {} + +type withChangeSetFunc func(context.Context, logr.Logger, *ssa.ChangeSet, *engine.ResourceSet) error + +func NewInstanceApplier(log logr.Logger, opts *CommonOptions, timeout time.Duration) *InstanceApplier { + applier := &InstanceApplier{ + opts: opts, + currentObjects: []*unstructured.Unstructured{}, + staleObjects: []*unstructured.Unstructured{}, + applyOptions: runtime.ApplyOptions(opts.Force, timeout), + waitOptions: ssa.WaitOptions{ + Interval: 5 * time.Second, + Timeout: timeout, + FailFast: true, + }, + progressStart: func(msg string) ProgressStopper { log.Info(msg) return &noopProgressStopper{} - } + }, } + applier.applyOptions.WaitInterval = applier.waitOptions.Interval + return applier +} + +func (a *InstanceApplier) Init(ctx context.Context, builder *engine.ModuleBuilder, buildResult cue.Value, instance *engine.BundleInstance, rcg genericclioptions.RESTClientGetter) error { finalValues, err := builder.GetDefaultValues() if err != nil { return fmt.Errorf("failed to extract values: %w", err) } - sets, err := builder.GetApplySets(buildResult) + a.sets, err = builder.GetApplySets(buildResult) if err != nil { return fmt.Errorf("failed to extract objects: %w", err) } - var objects []*unstructured.Unstructured - for _, set := range sets { - objects = append(objects, set.Objects...) + for _, set := range a.sets { + a.currentObjects = append(a.currentObjects, set.Objects...) } - rm, err := runtime.NewResourceManager(opts.KubeConfigFlags) + a.resourceManager, err = runtime.NewResourceManager(rcg) if err != nil { return err } - rm.SetOwnerLabels(objects, instance.Name, instance.Namespace) + a.resourceManager.SetOwnerLabels(a.currentObjects, instance.Name, instance.Namespace) - exists := false - sm := runtime.NewStorageManager(rm) - storedInstance, err := sm.Get(ctx, instance.Name, instance.Namespace) + a.storageManager = runtime.NewStorageManager(a.resourceManager) + storedInstance, err := a.storageManager.Get(ctx, instance.Name, instance.Namespace) if err == nil { - exists = true + a.instanceExists = true } - nsExists, err := sm.NamespaceExists(ctx, instance.Namespace) - if err != nil { - return fmt.Errorf("instance init failed: %w", err) - } + isStandaloneInstance := instance.Bundle == "" - if !opts.OverwriteOwnership && exists && isStandaloneInstance { + if !a.opts.OverwriteOwnership && a.instanceExists && isStandaloneInstance { if currentOwnerBundle := storedInstance.Labels[apiv1.BundleNameLabelKey]; currentOwnerBundle != "" { - return InstanceOwnershipConflictsErr(fmt.Sprintf("instance \"%s\" exists and is managed by bundle \"%s\"", instance.Name, currentOwnerBundle), "") + return &InstanceOwnershipConflictErr{{ + InstanceName: instance.Name, + CurrentOwnerBundle: currentOwnerBundle, + }} } } - im := runtime.NewInstanceManager(instance.Name, instance.Namespace, finalValues, instance.Module) + a.instanceManager = runtime.NewInstanceManager(instance.Name, instance.Namespace, finalValues, instance.Module) if !isStandaloneInstance { - if im.Instance.Labels == nil { - im.Instance.Labels = make(map[string]string) + if a.instanceManager.Instance.Labels == nil { + a.instanceManager.Instance.Labels = make(map[string]string) } - im.Instance.Labels[apiv1.BundleNameLabelKey] = instance.Bundle + a.instanceManager.Instance.Labels[apiv1.BundleNameLabelKey] = instance.Bundle } - if err := im.AddObjects(objects); err != nil { + if err := a.instanceManager.AddObjects(a.currentObjects); err != nil { return fmt.Errorf("adding objects to instance failed: %w", err) } - staleObjects, err := sm.GetStaleObjects(ctx, &im.Instance) + a.staleObjects, err = a.storageManager.GetStaleObjects(ctx, &a.instanceManager.Instance) if err != nil { return fmt.Errorf("getting stale objects failed: %w", err) } + return nil +} + +func (a *InstanceApplier) ApplyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleBuilder, buildResult cue.Value, opts InteractiveOptions) error { + if !a.instanceExists { + if err := a.UpdateStoredInstance(ctx); err != nil { + return fmt.Errorf("instance init failed: %w", err) + } + } + + return kerrors.NewAggregate([]error{ + a.ApplyAllSets(ctx, log, a.Wait), + a.PostApplyInventory(ctx, builder, buildResult), + a.PostApplyPruneStaleObjects(ctx, log, a.WaitForTermination), + }) +} + +func (a *InstanceApplier) ApplyInstanceInteractively(ctx context.Context, log logr.Logger, builder *engine.ModuleBuilder, buildResult cue.Value, opts InteractiveOptions) error { + if opts.DiffOutput == nil { + opts.DiffOutput = io.Discard + } + + if opts.ProgressStart != nil { + a.progressStart = opts.ProgressStart + } + + namespaceExists, err := a.NamespaceExists(ctx) + if err != nil { + return err + } if opts.DryRun || opts.Diff { - if !nsExists { - log.Info(logger.ColorizeJoin(logger.ColorizeSubject("Namespace/"+instance.Namespace), + if !namespaceExists { + log.Info(logger.ColorizeJoin(logger.ColorizeSubject("Namespace/"+a.Namespace()), ssa.CreatedAction, logger.DryRunServer)) } - if err := dyff.InstanceDryRunDiff( - logr.NewContext(ctx, log), - rm, - objects, - staleObjects, - nsExists, - opts.Dir, - opts.Diff, - opts.DiffOutput, - ); err != nil { + if err := a.DryRunDiff(logr.NewContext(ctx, log), namespaceExists, opts); err != nil { return err } @@ -151,95 +203,202 @@ func ApplyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleB return nil } - if !exists { + if !a.instanceExists { log.Info(fmt.Sprintf("installing %s in namespace %s", - logger.ColorizeSubject(instance.Name), logger.ColorizeSubject(instance.Namespace))) + logger.ColorizeSubject(a.Name()), logger.ColorizeSubject(a.Namespace()))) - if err := sm.Apply(ctx, &im.Instance, true); err != nil { + if err := a.UpdateStoredInstance(ctx); err != nil { return fmt.Errorf("instance init failed: %w", err) } - if !nsExists { - log.Info(logger.ColorizeJoin(logger.ColorizeSubject("Namespace/"+instance.Namespace), ssa.CreatedAction)) + if !namespaceExists { + log.Info(logger.ColorizeJoin(logger.ColorizeSubject("Namespace/"+a.Namespace()), ssa.CreatedAction)) } } else { log.Info(fmt.Sprintf("upgrading %s in namespace %s", - logger.ColorizeSubject(instance.Name), logger.ColorizeSubject(instance.Namespace))) + logger.ColorizeSubject(a.Name()), logger.ColorizeSubject(a.Namespace()))) } - applyOpts := runtime.ApplyOptions(opts.Force, timeout) - applyOpts.WaitInterval = 5 * time.Second + return kerrors.NewAggregate([]error{ + a.ApplyAllSets(ctx, log, a.WaitInteractive), + a.PostApplyInventory(ctx, builder, buildResult), + a.PostApplyPruneStaleObjects(ctx, log, a.WaitForTerminationInteractive), + }) +} - waitOptions := ssa.WaitOptions{ - Interval: applyOpts.WaitInterval, - Timeout: timeout, - FailFast: true, +func (a *InstanceApplier) Wait(ctx context.Context, log logr.Logger, _ *ssa.ChangeSet, rs *engine.ResourceSet) error { + doneMsg := "" + if rs != nil && rs.Name != "" { + doneMsg = fmt.Sprintf("%s resources ready", rs.Name) } + return a.doWait(ctx, log, rs, "waiting for %d resource(s) to become ready", doneMsg) +} - for _, set := range sets { - if len(sets) > 1 { +func (a *InstanceApplier) WaitInteractive(ctx context.Context, log logr.Logger, cs *ssa.ChangeSet, rs *engine.ResourceSet) error { + for _, change := range cs.Entries { + log.Info(logger.ColorizeJoin(change)) + } + doneMsg := "" + if rs != nil && rs.Name != "" { + doneMsg = fmt.Sprintf("%s resources %s", rs.Name, logger.ColorizeReady("ready")) + } + return a.doWait(ctx, log, rs, "waiting for %d resource(s) to become ready...", doneMsg) +} + +func (a *InstanceApplier) doWait(_ context.Context, log logr.Logger, rs *engine.ResourceSet, progressMsgFmt string, doneMsg string) error { + if !a.opts.Wait { + return nil + } + progress := a.progressStart(fmt.Sprintf(progressMsgFmt, len(rs.Objects))) + err := a.resourceManager.Wait(rs.Objects, a.waitOptions) + progress.Stop() + if err != nil { + return err + } + if doneMsg != "" { + doneMsg = "resources are ready" + } + log.Info(doneMsg) + return nil +} + +func (a *InstanceApplier) WaitForTermination(ctx context.Context, log logr.Logger, cs *ssa.ChangeSet, _ *engine.ResourceSet) error { + return a.doWaitForTermination(ctx, log, cs, "waiting for %d resource(s) to be finalized") +} + +func (a *InstanceApplier) WaitForTerminationInteractive(ctx context.Context, log logr.Logger, cs *ssa.ChangeSet, _ *engine.ResourceSet) error { + for _, change := range cs.Entries { + log.Info(logger.ColorizeJoin(change)) + } + return a.doWaitForTermination(ctx, log, cs, "waiting for %d resource(s) to be finalized...") +} + +func (a *InstanceApplier) doWaitForTermination(_ context.Context, log logr.Logger, cs *ssa.ChangeSet, progressMsgFmt string) error { + if !a.opts.Wait { + return nil + } + deletedObjects := runtime.SelectObjectsFromSet(cs, ssa.DeletedAction) + if len(deletedObjects) == 0 { + return nil + } + progress := a.progressStart(fmt.Sprintf(progressMsgFmt, len(deletedObjects))) + err := a.resourceManager.WaitForTermination(deletedObjects, a.waitOptions) + progress.Stop() + if err != nil { + return fmt.Errorf("waiting for termination failed: %w", err) + } + return nil +} + +func (a *InstanceApplier) ApplyAllSets(ctx context.Context, log logr.Logger, withChangeSet withChangeSetFunc) error { + if !a.instanceExists { + if err := a.UpdateStoredInstance(ctx); err != nil { + return fmt.Errorf("instance init failed: %w", err) + } + } + + multiSet := len(a.sets) > 1 + for s := range a.sets { + set := a.sets[s] + if multiSet { log.Info(fmt.Sprintf("applying %s", set.Name)) } - cs, err := rm.ApplyAllStaged(ctx, set.Objects, applyOpts) + cs, err := a.ApplyAllStaged(ctx, set) if err != nil { return err } - for _, change := range cs.Entries { - log.Info(logger.ColorizeJoin(change)) - } - if opts.Wait { - progress := opts.ProgressStart(fmt.Sprintf("waiting for %v resource(s) to become ready...", len(set.Objects))) - err = rm.Wait(set.Objects, waitOptions) - progress.Stop() - if err != nil { + if withChangeSet != nil { + if err := withChangeSet(ctx, log, cs, &set); err != nil { return err } - log.Info(fmt.Sprintf("%s resources %s", set.Name, logger.ColorizeReady("ready"))) } } + return nil +} - if images, err := builder.GetContainerImages(buildResult); err == nil { - im.Instance.Images = images +func (a *InstanceApplier) PostApplyPruneStaleObjects(ctx context.Context, log logr.Logger, withChangeSet withChangeSetFunc) error { + if len(a.staleObjects) == 0 { + return nil + } + deleteOpts := runtime.DeleteOptions(a.Name(), a.Namespace()) + cs, err := a.resourceManager.DeleteAll(ctx, a.staleObjects, deleteOpts) + if err != nil { + return fmt.Errorf("pruning objects failed: %w", err) + } + if withChangeSet != nil { + if err := withChangeSet(ctx, log, cs, nil); err != nil { + return err + } } + return nil +} - if err := sm.Apply(ctx, &im.Instance, true); err != nil { +func (a *InstanceApplier) PostApplyInventory(ctx context.Context, builder *engine.ModuleBuilder, buildResult cue.Value) error { + a.UpdateImages(builder, buildResult) + if err := a.UpdateStoredInstance(ctx); err != nil { return fmt.Errorf("storing instance failed: %w", err) } + return nil +} - var deletedObjects []*unstructured.Unstructured - if len(staleObjects) > 0 { - deleteOpts := runtime.DeleteOptions(instance.Name, instance.Namespace) - changeSet, err := rm.DeleteAll(ctx, staleObjects, deleteOpts) - if err != nil { - return fmt.Errorf("pruning objects failed: %w", err) - } - deletedObjects = runtime.SelectObjectsFromSet(changeSet, ssa.DeletedAction) - for _, change := range changeSet.Entries { - log.Info(logger.ColorizeJoin(change)) - } +func (a *InstanceApplier) UpdateStoredInstance(ctx context.Context) error { + return a.storageManager.Apply(ctx, &a.instanceManager.Instance, true) +} + +func (a *InstanceApplier) UpdateImages(builder *engine.ModuleBuilder, buildResult cue.Value) { + if images, err := builder.GetContainerImages(buildResult); err == nil { + a.instanceManager.Instance.Images = images } +} - if opts.Wait { - if len(deletedObjects) > 0 { - progress := opts.ProgressStart(fmt.Sprintf("waiting for %v resource(s) to be finalized...", len(deletedObjects))) - err = rm.WaitForTermination(deletedObjects, waitOptions) - progress.Stop() - if err != nil { - return fmt.Errorf("waiting for termination failed: %w", err) - } +func (a *InstanceApplier) Name() string { return a.instanceManager.Instance.Name } + +func (a *InstanceApplier) Namespace() string { return a.instanceManager.Instance.Namespace } + +func (a *InstanceApplier) NamespaceExists(ctx context.Context) (bool, error) { + ok, err := a.storageManager.NamespaceExists(ctx, a.Namespace()) + if err != nil { + return false, fmt.Errorf("cannot determine if namespace %q already exists: %w", a.Namespace(), err) + } + return ok, nil +} + +type InstanceOwnershipConflict struct{ InstanceName, CurrentOwnerBundle string } +type InstanceOwnershipConflictErr []InstanceOwnershipConflict + +func (e *InstanceOwnershipConflictErr) Error() string { + s := &strings.Builder{} + s.WriteString("instance ownership conflict encountered. ") + s.WriteString("Conflict: ") + numConflicts := len(*e) + for i, c := range *e { + if c.CurrentOwnerBundle != "" { + s.WriteString(fmt.Sprintf("instance %q exists and is managed by bundle %q", c.InstanceName, c.CurrentOwnerBundle)) + } else { + s.WriteString(fmt.Sprintf("instance %q exists and is managed by no bundle", c.InstanceName)) + } + if numConflicts > 1 && i != numConflicts { + s.WriteString("; ") } } + return s.String() +} - return nil +func (a *InstanceApplier) ApplyAllStaged(ctx context.Context, set engine.ResourceSet) (*ssa.ChangeSet, error) { + return a.resourceManager.ApplyAllStaged(ctx, set.Objects, a.applyOptions) } -func InstanceOwnershipConflictsErr(description, hint string) error { - msg := "instance ownership conflict encountered." - if hint != "" { - msg += " " + hint - } - msg += " Conflict: " + description - return errors.New(msg) +func (a *InstanceApplier) DryRunDiff(ctx context.Context, namespaceExists bool, opts InteractiveOptions) error { + return dyff.InstanceDryRunDiff( + ctx, + a.resourceManager, + a.currentObjects, + a.staleObjects, + namespaceExists, + a.opts.Dir, + opts.Diff, + opts.DiffOutput, + ) } From 3448c57e74f3cae5df6b59967d658c8b6d6d9ca3 Mon Sep 17 00:00:00 2001 From: Ilya Dmitrichenko Date: Wed, 24 Apr 2024 11:19:44 +0100 Subject: [PATCH 08/10] Naming improvements - rename `apply.InstanceApplier` to `reconciler.Reconciler` - use separate files inside the package - move interactive functions to dedicated `reconciler.InteractiveReconciler` type Signed-off-by: Ilya Dmitrichenko --- cmd/timoni/apply.go | 22 +- cmd/timoni/bundle_apply.go | 28 +- internal/apply/apply.go | 404 ----------------------------- internal/reconciler/interactive.go | 123 +++++++++ internal/reconciler/reconciler.go | 253 ++++++++++++++++++ internal/reconciler/types.go | 102 ++++++++ 6 files changed, 503 insertions(+), 429 deletions(-) delete mode 100644 internal/apply/apply.go create mode 100644 internal/reconciler/interactive.go create mode 100644 internal/reconciler/reconciler.go create mode 100644 internal/reconciler/types.go diff --git a/cmd/timoni/apply.go b/cmd/timoni/apply.go index 39e41991..895f98ae 100644 --- a/cmd/timoni/apply.go +++ b/cmd/timoni/apply.go @@ -27,10 +27,10 @@ import ( "github.com/spf13/cobra" apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" - "github.com/stefanprodan/timoni/internal/apply" "github.com/stefanprodan/timoni/internal/engine" "github.com/stefanprodan/timoni/internal/engine/fetcher" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/reconciler" "github.com/stefanprodan/timoni/internal/runtime" ) @@ -231,26 +231,26 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { Bundle: "", } - applier := apply.NewInstanceApplier(log, - &apply.CommonOptions{ + r := reconciler.NewInteractiveReconciler(log, + &reconciler.CommonOptions{ Dir: tmpDir, Wait: applyArgs.wait, Force: applyArgs.force, OverwriteOwnership: applyArgs.overwriteOwnership, }, + &reconciler.InteractiveOptions{ + DryRun: applyArgs.dryrun, + Diff: applyArgs.diff, + DiffOutput: cmd.OutOrStdout(), + // ProgressStart: logger.StartSpinner, + }, rootArgs.timeout, ) - if err := applier.Init(ctx, builder, buildResult, instance, kubeconfigArgs); err != nil { + if err := r.Init(ctx, builder, buildResult, instance, kubeconfigArgs); err != nil { return annotateInstanceOwnershipConflictErr(err) } - return applier.ApplyInstanceInteractively(ctx, log, + return r.ApplyInstance(ctx, log, builder, buildResult, - apply.InteractiveOptions{ - DryRun: applyArgs.dryrun, - Diff: applyArgs.diff, - DiffOutput: cmd.OutOrStdout(), - // ProgressStart: logger.StartSpinner, - }, ) } diff --git a/cmd/timoni/bundle_apply.go b/cmd/timoni/bundle_apply.go index c0676544..7dd2c1c0 100644 --- a/cmd/timoni/bundle_apply.go +++ b/cmd/timoni/bundle_apply.go @@ -32,11 +32,11 @@ import ( "github.com/spf13/cobra" apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" - "github.com/stefanprodan/timoni/internal/apply" "github.com/stefanprodan/timoni/internal/engine" "github.com/stefanprodan/timoni/internal/engine/fetcher" "github.com/stefanprodan/timoni/internal/flags" "github.com/stefanprodan/timoni/internal/logger" + "github.com/stefanprodan/timoni/internal/reconciler" "github.com/stefanprodan/timoni/internal/runtime" ) @@ -315,34 +315,34 @@ func applyBundleInstance(ctx context.Context, cuectx *cue.Context, instance *eng return describeErr(modDir, "build failed for "+instance.Name, err) } - applier := apply.NewInstanceApplier(log, - &apply.CommonOptions{ + r := reconciler.NewInteractiveReconciler(log, + &reconciler.CommonOptions{ Dir: rootDir, Wait: bundleApplyArgs.wait, Force: bundleApplyArgs.force, OverwriteOwnership: bundleApplyArgs.overwriteOwnership, }, + &reconciler.InteractiveOptions{ + DryRun: bundleApplyArgs.dryrun, + Diff: bundleApplyArgs.diff, + DiffOutput: diffOutput, + // ProgressStart: logger.StartSpinner, + }, rootArgs.timeout, ) - if err := applier.Init(ctx, builder, buildResult, instance, kubeconfigArgs); err != nil { + if err := r.Init(ctx, builder, buildResult, instance, kubeconfigArgs); err != nil { return annotateInstanceOwnershipConflictErr(err) } - return applier.ApplyInstanceInteractively(ctx, log, + return r.ApplyInstance(ctx, log, builder, buildResult, - apply.InteractiveOptions{ - DryRun: bundleApplyArgs.dryrun, - Diff: bundleApplyArgs.diff, - DiffOutput: diffOutput, - // ProgressStart: logger.StartSpinner, - }, ) } func annotateInstanceOwnershipConflictErr(err error) error { - if errors.Is(err, &apply.InstanceOwnershipConflictErr{}) { + if errors.Is(err, &reconciler.InstanceOwnershipConflictErr{}) { return fmt.Errorf("%s %s", err, "Apply with \"--overwrite-ownership\" to gain instance ownership.") } return err @@ -364,7 +364,7 @@ func saveReaderToFile(reader io.Reader) (string, error) { } func bundleInstancesOwnershipConflicts(bundleInstances []*engine.BundleInstance) error { - var conflicts apply.InstanceOwnershipConflictErr + var conflicts reconciler.InstanceOwnershipConflictErr rm, err := runtime.NewResourceManager(kubeconfigArgs) if err != nil { return err @@ -378,7 +378,7 @@ func bundleInstancesOwnershipConflicts(bundleInstances []*engine.BundleInstance) if existingInstance, err := sm.Get(ctx, instance.Name, instance.Namespace); err == nil { currentOwnerBundle := existingInstance.Labels[apiv1.BundleNameLabelKey] if currentOwnerBundle == "" || currentOwnerBundle != instance.Bundle { - conflicts = append(conflicts, apply.InstanceOwnershipConflict{ + conflicts = append(conflicts, reconciler.InstanceOwnershipConflict{ InstanceName: instance.Name, CurrentOwnerBundle: currentOwnerBundle, }) diff --git a/internal/apply/apply.go b/internal/apply/apply.go deleted file mode 100644 index 15436e1d..00000000 --- a/internal/apply/apply.go +++ /dev/null @@ -1,404 +0,0 @@ -/* -Copyright 2024 Stefan Prodan - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package apply - -import ( - "context" - "fmt" - "io" - "strings" - "time" - - "cuelang.org/go/cue" - "github.com/fluxcd/pkg/ssa" - "github.com/go-logr/logr" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - kerrors "k8s.io/apimachinery/pkg/util/errors" - "k8s.io/cli-runtime/pkg/genericclioptions" - - apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" - "github.com/stefanprodan/timoni/internal/dyff" - "github.com/stefanprodan/timoni/internal/engine" - "github.com/stefanprodan/timoni/internal/logger" - "github.com/stefanprodan/timoni/internal/runtime" -) - -type CommonOptions struct { - Dir string - Wait bool - Force bool - OverwriteOwnership bool -} - -type InteractiveOptions struct { - DryRun bool - Diff bool - DiffOutput io.Writer - - ProgressStart ProgressStarter -} - -type InstanceApplier struct { - opts *CommonOptions - - instanceExists bool - - sets []engine.ResourceSet - - currentObjects, staleObjects []*unstructured.Unstructured - - storageManager *runtime.StorageManager - instanceManager *runtime.InstanceManager - resourceManager *ssa.ResourceManager - - applyOptions ssa.ApplyOptions - waitOptions ssa.WaitOptions - - progressStart ProgressStarter -} - -type ( - ProgressStarter func(string) ProgressStopper - ProgressStopper interface{ Stop() } -) - -type noopProgressStopper struct{} - -func (n *noopProgressStopper) Stop() {} - -type withChangeSetFunc func(context.Context, logr.Logger, *ssa.ChangeSet, *engine.ResourceSet) error - -func NewInstanceApplier(log logr.Logger, opts *CommonOptions, timeout time.Duration) *InstanceApplier { - applier := &InstanceApplier{ - opts: opts, - currentObjects: []*unstructured.Unstructured{}, - staleObjects: []*unstructured.Unstructured{}, - applyOptions: runtime.ApplyOptions(opts.Force, timeout), - waitOptions: ssa.WaitOptions{ - Interval: 5 * time.Second, - Timeout: timeout, - FailFast: true, - }, - progressStart: func(msg string) ProgressStopper { - log.Info(msg) - return &noopProgressStopper{} - }, - } - applier.applyOptions.WaitInterval = applier.waitOptions.Interval - - return applier -} - -func (a *InstanceApplier) Init(ctx context.Context, builder *engine.ModuleBuilder, buildResult cue.Value, instance *engine.BundleInstance, rcg genericclioptions.RESTClientGetter) error { - finalValues, err := builder.GetDefaultValues() - if err != nil { - return fmt.Errorf("failed to extract values: %w", err) - } - - a.sets, err = builder.GetApplySets(buildResult) - if err != nil { - return fmt.Errorf("failed to extract objects: %w", err) - } - - for _, set := range a.sets { - a.currentObjects = append(a.currentObjects, set.Objects...) - } - - a.resourceManager, err = runtime.NewResourceManager(rcg) - if err != nil { - return err - } - - a.resourceManager.SetOwnerLabels(a.currentObjects, instance.Name, instance.Namespace) - - a.storageManager = runtime.NewStorageManager(a.resourceManager) - storedInstance, err := a.storageManager.Get(ctx, instance.Name, instance.Namespace) - if err == nil { - a.instanceExists = true - } - - isStandaloneInstance := instance.Bundle == "" - - if !a.opts.OverwriteOwnership && a.instanceExists && isStandaloneInstance { - if currentOwnerBundle := storedInstance.Labels[apiv1.BundleNameLabelKey]; currentOwnerBundle != "" { - return &InstanceOwnershipConflictErr{{ - InstanceName: instance.Name, - CurrentOwnerBundle: currentOwnerBundle, - }} - } - } - - a.instanceManager = runtime.NewInstanceManager(instance.Name, instance.Namespace, finalValues, instance.Module) - - if !isStandaloneInstance { - if a.instanceManager.Instance.Labels == nil { - a.instanceManager.Instance.Labels = make(map[string]string) - } - a.instanceManager.Instance.Labels[apiv1.BundleNameLabelKey] = instance.Bundle - } - - if err := a.instanceManager.AddObjects(a.currentObjects); err != nil { - return fmt.Errorf("adding objects to instance failed: %w", err) - } - - a.staleObjects, err = a.storageManager.GetStaleObjects(ctx, &a.instanceManager.Instance) - if err != nil { - return fmt.Errorf("getting stale objects failed: %w", err) - } - return nil -} - -func (a *InstanceApplier) ApplyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleBuilder, buildResult cue.Value, opts InteractiveOptions) error { - if !a.instanceExists { - if err := a.UpdateStoredInstance(ctx); err != nil { - return fmt.Errorf("instance init failed: %w", err) - } - } - - return kerrors.NewAggregate([]error{ - a.ApplyAllSets(ctx, log, a.Wait), - a.PostApplyInventory(ctx, builder, buildResult), - a.PostApplyPruneStaleObjects(ctx, log, a.WaitForTermination), - }) -} - -func (a *InstanceApplier) ApplyInstanceInteractively(ctx context.Context, log logr.Logger, builder *engine.ModuleBuilder, buildResult cue.Value, opts InteractiveOptions) error { - if opts.DiffOutput == nil { - opts.DiffOutput = io.Discard - } - - if opts.ProgressStart != nil { - a.progressStart = opts.ProgressStart - } - - namespaceExists, err := a.NamespaceExists(ctx) - if err != nil { - return err - } - - if opts.DryRun || opts.Diff { - if !namespaceExists { - log.Info(logger.ColorizeJoin(logger.ColorizeSubject("Namespace/"+a.Namespace()), - ssa.CreatedAction, logger.DryRunServer)) - } - if err := a.DryRunDiff(logr.NewContext(ctx, log), namespaceExists, opts); err != nil { - return err - } - - log.Info(logger.ColorizeJoin("applied successfully", logger.ColorizeDryRun("(server dry run)"))) - return nil - } - - if !a.instanceExists { - log.Info(fmt.Sprintf("installing %s in namespace %s", - logger.ColorizeSubject(a.Name()), logger.ColorizeSubject(a.Namespace()))) - - if err := a.UpdateStoredInstance(ctx); err != nil { - return fmt.Errorf("instance init failed: %w", err) - } - - if !namespaceExists { - log.Info(logger.ColorizeJoin(logger.ColorizeSubject("Namespace/"+a.Namespace()), ssa.CreatedAction)) - } - } else { - log.Info(fmt.Sprintf("upgrading %s in namespace %s", - logger.ColorizeSubject(a.Name()), logger.ColorizeSubject(a.Namespace()))) - } - - return kerrors.NewAggregate([]error{ - a.ApplyAllSets(ctx, log, a.WaitInteractive), - a.PostApplyInventory(ctx, builder, buildResult), - a.PostApplyPruneStaleObjects(ctx, log, a.WaitForTerminationInteractive), - }) -} - -func (a *InstanceApplier) Wait(ctx context.Context, log logr.Logger, _ *ssa.ChangeSet, rs *engine.ResourceSet) error { - doneMsg := "" - if rs != nil && rs.Name != "" { - doneMsg = fmt.Sprintf("%s resources ready", rs.Name) - } - return a.doWait(ctx, log, rs, "waiting for %d resource(s) to become ready", doneMsg) -} - -func (a *InstanceApplier) WaitInteractive(ctx context.Context, log logr.Logger, cs *ssa.ChangeSet, rs *engine.ResourceSet) error { - for _, change := range cs.Entries { - log.Info(logger.ColorizeJoin(change)) - } - doneMsg := "" - if rs != nil && rs.Name != "" { - doneMsg = fmt.Sprintf("%s resources %s", rs.Name, logger.ColorizeReady("ready")) - } - return a.doWait(ctx, log, rs, "waiting for %d resource(s) to become ready...", doneMsg) -} - -func (a *InstanceApplier) doWait(_ context.Context, log logr.Logger, rs *engine.ResourceSet, progressMsgFmt string, doneMsg string) error { - if !a.opts.Wait { - return nil - } - progress := a.progressStart(fmt.Sprintf(progressMsgFmt, len(rs.Objects))) - err := a.resourceManager.Wait(rs.Objects, a.waitOptions) - progress.Stop() - if err != nil { - return err - } - if doneMsg != "" { - doneMsg = "resources are ready" - } - log.Info(doneMsg) - return nil -} - -func (a *InstanceApplier) WaitForTermination(ctx context.Context, log logr.Logger, cs *ssa.ChangeSet, _ *engine.ResourceSet) error { - return a.doWaitForTermination(ctx, log, cs, "waiting for %d resource(s) to be finalized") -} - -func (a *InstanceApplier) WaitForTerminationInteractive(ctx context.Context, log logr.Logger, cs *ssa.ChangeSet, _ *engine.ResourceSet) error { - for _, change := range cs.Entries { - log.Info(logger.ColorizeJoin(change)) - } - return a.doWaitForTermination(ctx, log, cs, "waiting for %d resource(s) to be finalized...") -} - -func (a *InstanceApplier) doWaitForTermination(_ context.Context, log logr.Logger, cs *ssa.ChangeSet, progressMsgFmt string) error { - if !a.opts.Wait { - return nil - } - deletedObjects := runtime.SelectObjectsFromSet(cs, ssa.DeletedAction) - if len(deletedObjects) == 0 { - return nil - } - progress := a.progressStart(fmt.Sprintf(progressMsgFmt, len(deletedObjects))) - err := a.resourceManager.WaitForTermination(deletedObjects, a.waitOptions) - progress.Stop() - if err != nil { - return fmt.Errorf("waiting for termination failed: %w", err) - } - return nil -} - -func (a *InstanceApplier) ApplyAllSets(ctx context.Context, log logr.Logger, withChangeSet withChangeSetFunc) error { - if !a.instanceExists { - if err := a.UpdateStoredInstance(ctx); err != nil { - return fmt.Errorf("instance init failed: %w", err) - } - } - - multiSet := len(a.sets) > 1 - for s := range a.sets { - set := a.sets[s] - if multiSet { - log.Info(fmt.Sprintf("applying %s", set.Name)) - } - - cs, err := a.ApplyAllStaged(ctx, set) - if err != nil { - return err - } - - if withChangeSet != nil { - if err := withChangeSet(ctx, log, cs, &set); err != nil { - return err - } - } - } - return nil -} - -func (a *InstanceApplier) PostApplyPruneStaleObjects(ctx context.Context, log logr.Logger, withChangeSet withChangeSetFunc) error { - if len(a.staleObjects) == 0 { - return nil - } - deleteOpts := runtime.DeleteOptions(a.Name(), a.Namespace()) - cs, err := a.resourceManager.DeleteAll(ctx, a.staleObjects, deleteOpts) - if err != nil { - return fmt.Errorf("pruning objects failed: %w", err) - } - if withChangeSet != nil { - if err := withChangeSet(ctx, log, cs, nil); err != nil { - return err - } - } - return nil -} - -func (a *InstanceApplier) PostApplyInventory(ctx context.Context, builder *engine.ModuleBuilder, buildResult cue.Value) error { - a.UpdateImages(builder, buildResult) - if err := a.UpdateStoredInstance(ctx); err != nil { - return fmt.Errorf("storing instance failed: %w", err) - } - return nil -} - -func (a *InstanceApplier) UpdateStoredInstance(ctx context.Context) error { - return a.storageManager.Apply(ctx, &a.instanceManager.Instance, true) -} - -func (a *InstanceApplier) UpdateImages(builder *engine.ModuleBuilder, buildResult cue.Value) { - if images, err := builder.GetContainerImages(buildResult); err == nil { - a.instanceManager.Instance.Images = images - } -} - -func (a *InstanceApplier) Name() string { return a.instanceManager.Instance.Name } - -func (a *InstanceApplier) Namespace() string { return a.instanceManager.Instance.Namespace } - -func (a *InstanceApplier) NamespaceExists(ctx context.Context) (bool, error) { - ok, err := a.storageManager.NamespaceExists(ctx, a.Namespace()) - if err != nil { - return false, fmt.Errorf("cannot determine if namespace %q already exists: %w", a.Namespace(), err) - } - return ok, nil -} - -type InstanceOwnershipConflict struct{ InstanceName, CurrentOwnerBundle string } -type InstanceOwnershipConflictErr []InstanceOwnershipConflict - -func (e *InstanceOwnershipConflictErr) Error() string { - s := &strings.Builder{} - s.WriteString("instance ownership conflict encountered. ") - s.WriteString("Conflict: ") - numConflicts := len(*e) - for i, c := range *e { - if c.CurrentOwnerBundle != "" { - s.WriteString(fmt.Sprintf("instance %q exists and is managed by bundle %q", c.InstanceName, c.CurrentOwnerBundle)) - } else { - s.WriteString(fmt.Sprintf("instance %q exists and is managed by no bundle", c.InstanceName)) - } - if numConflicts > 1 && i != numConflicts { - s.WriteString("; ") - } - } - return s.String() -} - -func (a *InstanceApplier) ApplyAllStaged(ctx context.Context, set engine.ResourceSet) (*ssa.ChangeSet, error) { - return a.resourceManager.ApplyAllStaged(ctx, set.Objects, a.applyOptions) -} - -func (a *InstanceApplier) DryRunDiff(ctx context.Context, namespaceExists bool, opts InteractiveOptions) error { - return dyff.InstanceDryRunDiff( - ctx, - a.resourceManager, - a.currentObjects, - a.staleObjects, - namespaceExists, - a.opts.Dir, - opts.Diff, - opts.DiffOutput, - ) -} diff --git a/internal/reconciler/interactive.go b/internal/reconciler/interactive.go new file mode 100644 index 00000000..c581cb7f --- /dev/null +++ b/internal/reconciler/interactive.go @@ -0,0 +1,123 @@ +/* +Copyright 2024 Stefan Prodan + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "context" + "fmt" + "io" + "time" + + "cuelang.org/go/cue" + "github.com/fluxcd/pkg/ssa" + "github.com/go-logr/logr" + kerrors "k8s.io/apimachinery/pkg/util/errors" + + "github.com/stefanprodan/timoni/internal/dyff" + "github.com/stefanprodan/timoni/internal/engine" + "github.com/stefanprodan/timoni/internal/logger" +) + +func NewInteractiveReconciler(log logr.Logger, copts *CommonOptions, iopts *InteractiveOptions, timeout time.Duration) *InteractiveReconciler { + reconciler := &InteractiveReconciler{ + Reconciler: NewReconciler(log, copts, timeout), + InteractiveOptions: iopts, + } + + if reconciler.DiffOutput == nil { + reconciler.DiffOutput = io.Discard + } + + if iopts.ProgressStart != nil { + reconciler.progressStartFn = iopts.ProgressStart + } + + return reconciler +} + +func (r *InteractiveReconciler) ApplyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleBuilder, buildResult cue.Value) error { + namespaceExists, err := r.NamespaceExists(ctx) + if err != nil { + return err + } + + if r.DryRun || r.Diff { + if !namespaceExists { + log.Info(logger.ColorizeJoin(logger.ColorizeSubject("Namespace/"+r.Namespace()), + ssa.CreatedAction, logger.DryRunServer)) + } + if err := r.DryRunDiff(logr.NewContext(ctx, log), namespaceExists); err != nil { + return err + } + + log.Info(logger.ColorizeJoin("applied successfully", logger.ColorizeDryRun("(server dry run)"))) + return nil + } + + if !r.instanceExists { + log.Info(fmt.Sprintf("installing %s in namespace %s", + logger.ColorizeSubject(r.Name()), logger.ColorizeSubject(r.Namespace()))) + + if err := r.UpdateStoredInstance(ctx); err != nil { + return fmt.Errorf("instance init failed: %w", err) + } + + if !namespaceExists { + log.Info(logger.ColorizeJoin(logger.ColorizeSubject("Namespace/"+r.Namespace()), ssa.CreatedAction)) + } + } else { + log.Info(fmt.Sprintf("upgrading %s in namespace %s", + logger.ColorizeSubject(r.Name()), logger.ColorizeSubject(r.Namespace()))) + } + + return kerrors.NewAggregate([]error{ + r.ApplyAllSets(ctx, log, r.Wait), + r.PostApplyUpdateInventory(ctx, builder, buildResult), + r.PostApplyPruneStaleObjects(ctx, log, r.WaitForTermination), + }) +} + +func (r *InteractiveReconciler) DryRunDiff(ctx context.Context, namespaceExists bool) error { + return dyff.InstanceDryRunDiff( + ctx, + r.resourceManager, + r.currentObjects, + r.staleObjects, + namespaceExists, + r.opts.Dir, + r.Diff, + r.DiffOutput, + ) +} + +func (r *InteractiveReconciler) Wait(ctx context.Context, log logr.Logger, cs *ssa.ChangeSet, rs *engine.ResourceSet) error { + for _, change := range cs.Entries { + log.Info(logger.ColorizeJoin(change)) + } + doneMsg := "" + if rs != nil && rs.Name != "" { + doneMsg = fmt.Sprintf("%s resources %s", rs.Name, logger.ColorizeReady("ready")) + } + return r.doWait(ctx, log, rs, "waiting for %d resource(s) to become ready...", doneMsg) +} + +func (r *InteractiveReconciler) WaitForTermination(ctx context.Context, log logr.Logger, cs *ssa.ChangeSet, _ *engine.ResourceSet) error { + for _, change := range cs.Entries { + log.Info(logger.ColorizeJoin(change)) + } + return r.doWaitForTermination(ctx, log, cs, "waiting for %d resource(s) to be finalized...") +} diff --git a/internal/reconciler/reconciler.go b/internal/reconciler/reconciler.go new file mode 100644 index 00000000..eaffdfb1 --- /dev/null +++ b/internal/reconciler/reconciler.go @@ -0,0 +1,253 @@ +/* +Copyright 2024 Stefan Prodan + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "context" + "fmt" + "time" + + "cuelang.org/go/cue" + "github.com/fluxcd/pkg/ssa" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + kerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/cli-runtime/pkg/genericclioptions" + + apiv1 "github.com/stefanprodan/timoni/api/v1alpha1" + "github.com/stefanprodan/timoni/internal/engine" + "github.com/stefanprodan/timoni/internal/runtime" +) + +func NewReconciler(log logr.Logger, opts *CommonOptions, timeout time.Duration) *Reconciler { + reconciler := &Reconciler{ + opts: opts, + currentObjects: []*unstructured.Unstructured{}, + staleObjects: []*unstructured.Unstructured{}, + applyOptions: runtime.ApplyOptions(opts.Force, timeout), + waitOptions: ssa.WaitOptions{ + Interval: 5 * time.Second, + Timeout: timeout, + FailFast: true, + }, + progressStartFn: func(msg string) interface{ Stop() } { + log.Info(msg) + return &noopProgressStopper{} + }, + } + reconciler.applyOptions.WaitInterval = reconciler.waitOptions.Interval + + return reconciler +} + +func (r *Reconciler) Init(ctx context.Context, builder *engine.ModuleBuilder, buildResult cue.Value, instance *engine.BundleInstance, rcg genericclioptions.RESTClientGetter) error { + finalValues, err := builder.GetDefaultValues() + if err != nil { + return fmt.Errorf("failed to extract values: %w", err) + } + + r.sets, err = builder.GetApplySets(buildResult) + if err != nil { + return fmt.Errorf("failed to extract objects: %w", err) + } + + for _, set := range r.sets { + r.currentObjects = append(r.currentObjects, set.Objects...) + } + + r.resourceManager, err = runtime.NewResourceManager(rcg) + if err != nil { + return err + } + + r.resourceManager.SetOwnerLabels(r.currentObjects, instance.Name, instance.Namespace) + + r.storageManager = runtime.NewStorageManager(r.resourceManager) + storedInstance, err := r.storageManager.Get(ctx, instance.Name, instance.Namespace) + if err == nil { + r.instanceExists = true + } + + isStandaloneInstance := instance.Bundle == "" + + if !r.opts.OverwriteOwnership && r.instanceExists && isStandaloneInstance { + if currentOwnerBundle := storedInstance.Labels[apiv1.BundleNameLabelKey]; currentOwnerBundle != "" { + return &InstanceOwnershipConflictErr{{ + InstanceName: instance.Name, + CurrentOwnerBundle: currentOwnerBundle, + }} + } + } + + r.instanceManager = runtime.NewInstanceManager(instance.Name, instance.Namespace, finalValues, instance.Module) + + if !isStandaloneInstance { + if r.instanceManager.Instance.Labels == nil { + r.instanceManager.Instance.Labels = make(map[string]string) + } + r.instanceManager.Instance.Labels[apiv1.BundleNameLabelKey] = instance.Bundle + } + + if err := r.instanceManager.AddObjects(r.currentObjects); err != nil { + return fmt.Errorf("adding objects to instance failed: %w", err) + } + + r.staleObjects, err = r.storageManager.GetStaleObjects(ctx, &r.instanceManager.Instance) + if err != nil { + return fmt.Errorf("getting stale objects failed: %w", err) + } + return nil +} + +func (r *Reconciler) ApplyInstance(ctx context.Context, log logr.Logger, builder *engine.ModuleBuilder, buildResult cue.Value, opts InteractiveOptions) error { + if !r.instanceExists { + if err := r.UpdateStoredInstance(ctx); err != nil { + return fmt.Errorf("instance init failed: %w", err) + } + } + + return kerrors.NewAggregate([]error{ + r.ApplyAllSets(ctx, log, r.Wait), + r.PostApplyUpdateInventory(ctx, builder, buildResult), + r.PostApplyPruneStaleObjects(ctx, log, r.WaitForTermination), + }) +} + +func (a *Reconciler) Wait(ctx context.Context, log logr.Logger, _ *ssa.ChangeSet, rs *engine.ResourceSet) error { + doneMsg := "" + if rs != nil && rs.Name != "" { + doneMsg = fmt.Sprintf("%s resources ready", rs.Name) + } + return a.doWait(ctx, log, rs, "waiting for %d resource(s) to become ready", doneMsg) +} + +func (r *Reconciler) doWait(_ context.Context, log logr.Logger, rs *engine.ResourceSet, progressMsgFmt string, doneMsg string) error { + if !r.opts.Wait { + return nil + } + progress := r.progressStartFn(fmt.Sprintf(progressMsgFmt, len(rs.Objects))) + err := r.resourceManager.Wait(rs.Objects, r.waitOptions) + progress.Stop() + if err != nil { + return err + } + if doneMsg != "" { + doneMsg = "resources are ready" + } + log.Info(doneMsg) + return nil +} + +func (r *Reconciler) WaitForTermination(ctx context.Context, log logr.Logger, cs *ssa.ChangeSet, _ *engine.ResourceSet) error { + return r.doWaitForTermination(ctx, log, cs, "waiting for %d resource(s) to be finalized") +} + +func (r *Reconciler) doWaitForTermination(_ context.Context, _ logr.Logger, cs *ssa.ChangeSet, progressMsgFmt string) error { + if !r.opts.Wait { + return nil + } + deletedObjects := runtime.SelectObjectsFromSet(cs, ssa.DeletedAction) + if len(deletedObjects) == 0 { + return nil + } + progress := r.progressStartFn(fmt.Sprintf(progressMsgFmt, len(deletedObjects))) + err := r.resourceManager.WaitForTermination(deletedObjects, r.waitOptions) + progress.Stop() + if err != nil { + return fmt.Errorf("waiting for termination failed: %w", err) + } + return nil +} + +func (r *Reconciler) ApplyAllSets(ctx context.Context, log logr.Logger, withChangeSet withChangeSetFunc) error { + if !r.instanceExists { + if err := r.UpdateStoredInstance(ctx); err != nil { + return fmt.Errorf("instance init failed: %w", err) + } + } + + multiSet := len(r.sets) > 1 + for s := range r.sets { + set := r.sets[s] + if multiSet { + log.Info(fmt.Sprintf("applying %s", set.Name)) + } + + cs, err := r.ApplyAllStaged(ctx, set) + if err != nil { + return err + } + + if withChangeSet != nil { + if err := withChangeSet(ctx, log, cs, &set); err != nil { + return err + } + } + } + return nil +} + +func (r *Reconciler) ApplyAllStaged(ctx context.Context, set engine.ResourceSet) (*ssa.ChangeSet, error) { + return r.resourceManager.ApplyAllStaged(ctx, set.Objects, r.applyOptions) +} + +func (r *Reconciler) PostApplyPruneStaleObjects(ctx context.Context, log logr.Logger, withChangeSet withChangeSetFunc) error { + if len(r.staleObjects) == 0 { + return nil + } + deleteOpts := runtime.DeleteOptions(r.Name(), r.Namespace()) + cs, err := r.resourceManager.DeleteAll(ctx, r.staleObjects, deleteOpts) + if err != nil { + return fmt.Errorf("pruning objects failed: %w", err) + } + if withChangeSet != nil { + if err := withChangeSet(ctx, log, cs, nil); err != nil { + return err + } + } + return nil +} + +func (r *Reconciler) PostApplyUpdateInventory(ctx context.Context, builder *engine.ModuleBuilder, buildResult cue.Value) error { + r.UpdateImages(builder, buildResult) + if err := r.UpdateStoredInstance(ctx); err != nil { + return fmt.Errorf("storing instance failed: %w", err) + } + return nil +} + +func (r *Reconciler) UpdateStoredInstance(ctx context.Context) error { + return r.storageManager.Apply(ctx, &r.instanceManager.Instance, true) +} + +func (r *Reconciler) UpdateImages(builder *engine.ModuleBuilder, buildResult cue.Value) { + if images, err := builder.GetContainerImages(buildResult); err == nil { + r.instanceManager.Instance.Images = images + } +} + +func (r *Reconciler) Name() string { return r.instanceManager.Instance.Name } + +func (r *Reconciler) Namespace() string { return r.instanceManager.Instance.Namespace } + +func (r *Reconciler) NamespaceExists(ctx context.Context) (bool, error) { + ok, err := r.storageManager.NamespaceExists(ctx, r.Namespace()) + if err != nil { + return false, fmt.Errorf("cannot determine if namespace %q already exists: %w", r.Namespace(), err) + } + return ok, nil +} diff --git a/internal/reconciler/types.go b/internal/reconciler/types.go new file mode 100644 index 00000000..57022b6a --- /dev/null +++ b/internal/reconciler/types.go @@ -0,0 +1,102 @@ +/* +Copyright 2024 Stefan Prodan + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package reconciler + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/fluxcd/pkg/ssa" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/stefanprodan/timoni/internal/engine" + "github.com/stefanprodan/timoni/internal/runtime" +) + +type CommonOptions struct { + Dir string + Wait bool + Force bool + OverwriteOwnership bool +} + +type InteractiveOptions struct { + DryRun bool + Diff bool + DiffOutput io.Writer + + ProgressStart ProgressStarter +} + +type Reconciler struct { + opts *CommonOptions + + instanceExists bool + + sets []engine.ResourceSet + + currentObjects, staleObjects []*unstructured.Unstructured + + storageManager *runtime.StorageManager + instanceManager *runtime.InstanceManager + resourceManager *ssa.ResourceManager + + applyOptions ssa.ApplyOptions + waitOptions ssa.WaitOptions + + progressStartFn ProgressStarter +} + +type InteractiveReconciler struct { + *Reconciler + *InteractiveOptions +} + +type ( + ProgressStarter func(string) ProgressStopper + ProgressStopper interface{ Stop() } +) + +type noopProgressStopper struct{} + +func (*noopProgressStopper) Stop() {} + +type withChangeSetFunc func(context.Context, logr.Logger, *ssa.ChangeSet, *engine.ResourceSet) error + +type InstanceOwnershipConflict struct{ InstanceName, CurrentOwnerBundle string } +type InstanceOwnershipConflictErr []InstanceOwnershipConflict + +func (e *InstanceOwnershipConflictErr) Error() string { + s := &strings.Builder{} + s.WriteString("instance ownership conflict encountered. ") + s.WriteString("Conflict: ") + numConflicts := len(*e) + for i, c := range *e { + if c.CurrentOwnerBundle != "" { + s.WriteString(fmt.Sprintf("instance %q exists and is managed by bundle %q", c.InstanceName, c.CurrentOwnerBundle)) + } else { + s.WriteString(fmt.Sprintf("instance %q exists and is managed by no bundle", c.InstanceName)) + } + if numConflicts > 1 && i != numConflicts { + s.WriteString("; ") + } + } + return s.String() +} From 156409964b0e50475d21b2271675d320cdb4eca8 Mon Sep 17 00:00:00 2001 From: Ilya Dmitrichenko Date: Wed, 24 Apr 2024 11:44:06 +0100 Subject: [PATCH 09/10] Simplify inteface definition to work with a bare function It turns out that a bare function is not treated the same way as a struct when it comes to matching it to an interface Signed-off-by: Ilya Dmitrichenko --- cmd/timoni/apply.go | 9 +++++---- cmd/timoni/bundle_apply.go | 8 ++++---- internal/logger/logger.go | 2 +- internal/reconciler/types.go | 9 ++------- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/cmd/timoni/apply.go b/cmd/timoni/apply.go index 895f98ae..28a0274d 100644 --- a/cmd/timoni/apply.go +++ b/cmd/timoni/apply.go @@ -30,6 +30,7 @@ import ( "github.com/stefanprodan/timoni/internal/engine" "github.com/stefanprodan/timoni/internal/engine/fetcher" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/reconciler" "github.com/stefanprodan/timoni/internal/runtime" ) @@ -239,10 +240,10 @@ func runApplyCmd(cmd *cobra.Command, args []string) error { OverwriteOwnership: applyArgs.overwriteOwnership, }, &reconciler.InteractiveOptions{ - DryRun: applyArgs.dryrun, - Diff: applyArgs.diff, - DiffOutput: cmd.OutOrStdout(), - // ProgressStart: logger.StartSpinner, + DryRun: applyArgs.dryrun, + Diff: applyArgs.diff, + DiffOutput: cmd.OutOrStdout(), + ProgressStart: logger.StartSpinner, }, rootArgs.timeout, ) diff --git a/cmd/timoni/bundle_apply.go b/cmd/timoni/bundle_apply.go index 7dd2c1c0..ed8af239 100644 --- a/cmd/timoni/bundle_apply.go +++ b/cmd/timoni/bundle_apply.go @@ -323,10 +323,10 @@ func applyBundleInstance(ctx context.Context, cuectx *cue.Context, instance *eng OverwriteOwnership: bundleApplyArgs.overwriteOwnership, }, &reconciler.InteractiveOptions{ - DryRun: bundleApplyArgs.dryrun, - Diff: bundleApplyArgs.diff, - DiffOutput: diffOutput, - // ProgressStart: logger.StartSpinner, + DryRun: bundleApplyArgs.dryrun, + Diff: bundleApplyArgs.diff, + DiffOutput: diffOutput, + ProgressStart: logger.StartSpinner, }, rootArgs.timeout, ) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 0db25df4..a999cbce 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -196,7 +196,7 @@ func ColorizeCluster(cluster string) string { } // StartSpinner starts a spinner with the given message. -func StartSpinner(msg string) *spinner.Spinner { +func StartSpinner(msg string) interface{ Stop() } { s := spinner.New(spinner.CharSets[11], 100*time.Millisecond, spinner.WithWriter(os.Stderr)) s.Suffix = " " + msg s.Start() diff --git a/internal/reconciler/types.go b/internal/reconciler/types.go index 57022b6a..79aa990f 100644 --- a/internal/reconciler/types.go +++ b/internal/reconciler/types.go @@ -42,7 +42,7 @@ type InteractiveOptions struct { Diff bool DiffOutput io.Writer - ProgressStart ProgressStarter + ProgressStart func(string) interface{ Stop() } } type Reconciler struct { @@ -61,7 +61,7 @@ type Reconciler struct { applyOptions ssa.ApplyOptions waitOptions ssa.WaitOptions - progressStartFn ProgressStarter + progressStartFn func(string) interface{ Stop() } } type InteractiveReconciler struct { @@ -69,11 +69,6 @@ type InteractiveReconciler struct { *InteractiveOptions } -type ( - ProgressStarter func(string) ProgressStopper - ProgressStopper interface{ Stop() } -) - type noopProgressStopper struct{} func (*noopProgressStopper) Stop() {} From b8686f0214dc42abac452f2a50bf82f764612ffc Mon Sep 17 00:00:00 2001 From: Ilya Dmitrichenko Date: Wed, 24 Apr 2024 11:57:13 +0100 Subject: [PATCH 10/10] Refine ownership conflict error messages and fix associated tests Signed-off-by: Ilya Dmitrichenko --- cmd/timoni/apply_test.go | 2 +- cmd/timoni/bundle_apply_test.go | 6 +++--- internal/reconciler/types.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/timoni/apply_test.go b/cmd/timoni/apply_test.go index 471010c4..f4ffc104 100644 --- a/cmd/timoni/apply_test.go +++ b/cmd/timoni/apply_test.go @@ -235,7 +235,7 @@ bundle: { modPath, )) g.Expect(err).To(HaveOccurred()) - g.Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("instance \"%s\" exists and is managed by bundle \"%s\"", instanceName, bundleName))) + g.Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("instance \"%s\" exists and is managed by another bundle \"%s\"", instanceName, bundleName))) output, err := executeCommand(fmt.Sprintf("ls -n %[1]s", namespace)) g.Expect(err).ToNot(HaveOccurred()) diff --git a/cmd/timoni/bundle_apply_test.go b/cmd/timoni/bundle_apply_test.go index 272ea702..31b480d9 100644 --- a/cmd/timoni/bundle_apply_test.go +++ b/cmd/timoni/bundle_apply_test.go @@ -155,8 +155,8 @@ bundle: { _, err = executeCommandWithIn("bundle apply -f - -p main --wait", strings.NewReader(anotherBundleData)) g.Expect(err).To(HaveOccurred()) - g.Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("instance \"%s\" exists and is managed by another bundle \"%s\"", "frontend", bundleName))) - g.Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("instance \"%s\" exists and is managed by another bundle \"%s\"", "backend", bundleName))) + g.Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("instance %q exists and is managed by another bundle %q", "frontend", bundleName))) + g.Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("instance %q exists and is managed by another bundle %q", "backend", bundleName))) }) t.Run("fails to create instances from partially overlapping bundle", func(t *testing.T) { @@ -247,7 +247,7 @@ bundle: { _, err = executeCommandWithIn("bundle apply -f - -p main --wait", strings.NewReader(bundleData)) g.Expect(err).To(HaveOccurred()) - g.Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("instance \"%s\" exists and is managed by no bundle", instanceName))) + g.Expect(err.Error()).To(ContainSubstring(fmt.Sprintf("instance \"%s\" exists and is not managed by any bundle", instanceName))) }) } diff --git a/internal/reconciler/types.go b/internal/reconciler/types.go index 79aa990f..41dafda9 100644 --- a/internal/reconciler/types.go +++ b/internal/reconciler/types.go @@ -85,9 +85,9 @@ func (e *InstanceOwnershipConflictErr) Error() string { numConflicts := len(*e) for i, c := range *e { if c.CurrentOwnerBundle != "" { - s.WriteString(fmt.Sprintf("instance %q exists and is managed by bundle %q", c.InstanceName, c.CurrentOwnerBundle)) + s.WriteString(fmt.Sprintf("instance %q exists and is managed by another bundle %q", c.InstanceName, c.CurrentOwnerBundle)) } else { - s.WriteString(fmt.Sprintf("instance %q exists and is managed by no bundle", c.InstanceName)) + s.WriteString(fmt.Sprintf("instance %q exists and is not managed by any bundle", c.InstanceName)) } if numConflicts > 1 && i != numConflicts { s.WriteString("; ")