diff --git a/cmd/timoni/apply.go b/cmd/timoni/apply.go index ce77ba2e..086ece95 100644 --- a/cmd/timoni/apply.go +++ b/cmd/timoni/apply.go @@ -27,10 +27,12 @@ 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" cueerrors "github.com/stefanprodan/timoni/internal/errors" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/runtime" ) @@ -112,6 +114,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()) @@ -139,7 +143,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 == "" { @@ -224,13 +228,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{ @@ -240,5 +248,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 6c187cfd..737d1810 100644 --- a/cmd/timoni/bundle_apply.go +++ b/cmd/timoni/bundle_apply.go @@ -29,16 +29,16 @@ 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" cueerrors "github.com/stefanprodan/timoni/internal/errors" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/runtime" runtimebuild "github.com/stefanprodan/timoni/internal/runtime/build" ) @@ -196,7 +196,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) @@ -206,7 +206,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 { @@ -221,18 +221,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 } } @@ -240,7 +240,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))) } @@ -285,8 +285,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( @@ -308,7 +308,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 @@ -321,190 +321,35 @@ func applyBundleInstance(ctx context.Context, cuectx *cue.Context, instance *eng return cueerrors.Describe(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 { @@ -529,27 +374,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 d289471f..a499fb31 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" runtimebuild "github.com/stefanprodan/timoni/internal/runtime/build" ) @@ -124,7 +125,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") @@ -135,7 +136,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, @@ -150,7 +151,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 { @@ -176,7 +177,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 } @@ -192,7 +193,7 @@ func deleteBundleInstance(ctx context.Context, instance *engine.BundleInstance, continue } cs.Add(*change) - log.Info(colorizeJoin(change)) + log.Info(logger.ColorizeJoin(change)) } if hasErrors { @@ -207,7 +208,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 dac2887c..cf25cf78 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" runtimebuild "github.com/stefanprodan/timoni/internal/runtime/build" @@ -107,7 +108,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") @@ -116,18 +117,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}} @@ -141,23 +142,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 5acb6dbf..051cb98e 100644 --- a/cmd/timoni/bundle_vet.go +++ b/cmd/timoni/bundle_vet.go @@ -32,6 +32,7 @@ import ( "github.com/stefanprodan/timoni/internal/engine" cueerrors "github.com/stefanprodan/timoni/internal/errors" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/runtime" runtimebuild "github.com/stefanprodan/timoni/internal/runtime/build" ) @@ -80,7 +81,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") @@ -174,7 +175,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") @@ -198,7 +199,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 c55d090b..e0fa1c02 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) ) @@ -80,7 +82,7 @@ func init() { rootCmd.PersistentFlags().BoolVar(&rootArgs.prettyLog, "log-pretty", rootArgs.prettyLog, "Adds timestamps to the logs.") rootCmd.PersistentFlags().BoolVar(&rootArgs.coloredLog, "log-color", rootArgs.coloredLog, - "Adds colorized output to the logs. (defaults to false when no tty)") + "Adds logger.Colorized output to the logs. (defaults to false when no tty)") rootCmd.PersistentFlags().StringVar(&rootArgs.cacheDir, "cache-dir", "", "Artifacts cache dir, can be disable with 'TIMONI_CACHING=false' env var. (defaults to \"$HOME/.timoni/cache\")") rootCmd.PersistentFlags().BoolVar(&rootArgs.registryInsecure, "registry-insecure", 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/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 55237a33..c2ac9f79 100644 --- a/cmd/timoni/mod_vet.go +++ b/cmd/timoni/mod_vet.go @@ -35,6 +35,7 @@ import ( "github.com/stefanprodan/timoni/internal/engine/fetcher" cueerrors "github.com/stefanprodan/timoni/internal/errors" "github.com/stefanprodan/timoni/internal/flags" + "github.com/stefanprodan/timoni/internal/logger" ) var vetModCmd = &cobra.Command{ @@ -82,7 +83,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) @@ -180,7 +181,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) @@ -196,15 +197,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 fafae0f1..dc659bb2 100644 --- a/cmd/timoni/runtime_build.go +++ b/cmd/timoni/runtime_build.go @@ -25,6 +25,7 @@ import ( "github.com/spf13/cobra" + "github.com/stefanprodan/timoni/internal/logger" "github.com/stefanprodan/timoni/internal/runtime" runtimebuild "github.com/stefanprodan/timoni/internal/runtime/build" ) @@ -94,7 +95,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) @@ -117,7 +118,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 65% rename from cmd/timoni/log.go rename to internal/logger/logger.go index d130cd00..afb70b1b 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 { +// Pretty print adds timestamp, log level and logger.Colorized output to the logs. +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,150 @@ 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 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 {