From 22871d1fc259feec691ad428d33b326535793858 Mon Sep 17 00:00:00 2001 From: Stefan Prodan Date: Sun, 8 Oct 2023 19:48:51 +0300 Subject: [PATCH] Refactor bundle delete command Simplify the delete command to `timoni bundle delete ` and deprecate `--name`. When deleting a bundle by referring to a file with `timoni bundle delete -f bundle.cue`, the name is extracted from the file without parsing the instances or any other fields. Signed-off-by: Stefan Prodan --- cmd/timoni/bundle_apply_test.go | 2 +- cmd/timoni/bundle_delete.go | 113 +++++++++---------------------- cmd/timoni/bundle_delete_test.go | 29 +------- docs/bundle.md | 17 +++-- internal/engine/utils.go | 25 +++++++ 5 files changed, 70 insertions(+), 116 deletions(-) diff --git a/cmd/timoni/bundle_apply_test.go b/cmd/timoni/bundle_apply_test.go index a001f081..da8169b4 100644 --- a/cmd/timoni/bundle_apply_test.go +++ b/cmd/timoni/bundle_apply_test.go @@ -228,7 +228,7 @@ bundle: { g.Expect(output).To(ContainSubstring(anotherBundleName)) t.Cleanup(func() { - _, err = executeCommand(fmt.Sprintf("bundle delete --name %[1]s -n %[2]s", anotherBundleName, namespace)) + _, err = executeCommand(fmt.Sprintf("bundle delete %s --wait", anotherBundleName)) g.Expect(err).ToNot(HaveOccurred()) }) }) diff --git a/cmd/timoni/bundle_delete.go b/cmd/timoni/bundle_delete.go index c19a9365..63111e86 100644 --- a/cmd/timoni/bundle_delete.go +++ b/cmd/timoni/bundle_delete.go @@ -33,7 +33,7 @@ import ( var bundleDelCmd = &cobra.Command{ Use: "delete", - Aliases: []string{"rm"}, + Aliases: []string{"rm", "uninstall"}, Short: "Delete all instances from a bundle", Long: `The bundle delete command uninstalls the instances and deletes all their Kubernetes resources from the cluster.'. @@ -42,62 +42,68 @@ deletes all their Kubernetes resources from the cluster.'. timoni bundle delete -f bundle.cue # Uninstall all instances in a named bundle - timoni bundle delete --name podinfo + timoni bundle delete my-app # Uninstall all instances without waiting for finalisation - timoni bundle delete -f bundle.cue --wait=false + timoni bundle delete my-app --wait=false # Do a dry-run uninstall and print the changes - timoni bundle delete -f bundle.cue --dry-run + timoni bundle delete my-app --dry-run `, RunE: runBundleDelCmd, } type bundleDelFlags struct { - files []string - allNamespaces bool - wait bool - dryrun bool - name string + filename string + wait bool + dryrun bool + name string } var bundleDelArgs bundleDelFlags func init() { - bundleDelCmd.Flags().StringSliceVarP(&bundleDelArgs.files, "file", "f", nil, - "The local path to bundle.cue files.") bundleDelCmd.Flags().BoolVar(&bundleDelArgs.wait, "wait", true, "Wait for the deleted Kubernetes objects to be finalized.") bundleDelCmd.Flags().BoolVar(&bundleDelArgs.dryrun, "dry-run", false, "Perform a server-side delete dry run.") + bundleDelCmd.Flags().StringVarP(&bundleDelArgs.filename, "file", "f", "", + "The local path to bundle.cue file.") bundleDelCmd.Flags().StringVar(&bundleDelArgs.name, "name", "", "Name of the bundle to delete.") - bundleDelCmd.Flags().BoolVarP(&bundleDelArgs.allNamespaces, "all-namespaces", "A", false, - "Delete the requested Bundle across all namespaces.") + bundleDelCmd.Flags().MarkDeprecated("name", "use 'timoni bundle delete '") bundleCmd.AddCommand(bundleDelCmd) } -func runBundleDelCmd(cmd *cobra.Command, _ []string) error { - ctx, cancel := context.WithTimeout(cmd.Context(), rootArgs.timeout) - defer cancel() +func runBundleDelCmd(cmd *cobra.Command, args []string) error { + if len(args) < 1 && bundleDelArgs.filename == "" && bundleDelArgs.name == "" { + return fmt.Errorf("bundle name is required") + } - if bundleDelArgs.name != "" { - return deleteBundleByName(ctx, bundleDelArgs.name) + switch { + case bundleDelArgs.filename != "": + cuectx := cuecontext.New() + name, err := engine.ExtractStringFromFile(cuectx, bundleDelArgs.filename, apiv1.BundleName.String()) + if err != nil { + return err + } + bundleDelArgs.name = name + case len(args) == 1: + bundleDelArgs.name = args[0] } - return deleteBundleFromFile(ctx, cmd) -} + ctx, cancel := context.WithTimeout(cmd.Context(), rootArgs.timeout) + defer cancel() -func deleteBundleByName(ctx context.Context, bundle string) error { sm, err := runtime.NewResourceManager(kubeconfigArgs) if err != nil { return err } - log := LoggerBundle(ctx, bundle) + log := LoggerBundle(ctx, bundleDelArgs.name) iStorage := runtime.NewStorageManager(sm) - instances, err := iStorage.List(ctx, "", bundle) + instances, err := iStorage.List(ctx, "", bundleDelArgs.name) if err != nil { return err } @@ -109,70 +115,16 @@ func deleteBundleByName(ctx context.Context, bundle string) error { // delete in revers order (last installed, first to uninstall) for index := len(instances) - 1; index >= 0; index-- { instance := instances[index] - log.Info(fmt.Sprintf("deleting instance %s from bundle %s", instance.Name, bundleDelArgs.name)) + log.Info(fmt.Sprintf("deleting instance %s in namespace %s", + colorizeSubject(instance.Name), colorizeSubject(instance.Namespace))) if err := deleteBundleInstance(ctx, &engine.BundleInstance{ - Bundle: bundle, + Bundle: bundleDelArgs.name, Name: instance.Name, Namespace: instance.Namespace, }, bundleDelArgs.wait, bundleDelArgs.dryrun); err != nil { return err } } - - return nil -} - -func deleteBundleFromFile(ctx context.Context, cmd *cobra.Command) error { - bundleSchema, err := os.CreateTemp("", "schema.*.cue") - if err != nil { - return err - } - defer os.Remove(bundleSchema.Name()) - if _, err := bundleSchema.WriteString(apiv1.BundleSchema); err != nil { - return err - } - - files := append(bundleDelArgs.files, bundleSchema.Name()) - for i, file := range files { - if file == "-" { - path, err := saveReaderToFile(cmd.InOrStdin()) - if err != nil { - return err - } - - defer os.Remove(path) - - files[i] = path - } - } - - cuectx := cuecontext.New() - bm := engine.NewBundleBuilder(cuectx, files) - - v, err := bm.Build() - if err != nil { - return err - } - - bundle, err := bm.GetBundle(v) - if err != nil { - return err - } - - log := LoggerBundle(ctx, bundle.Name) - - if len(bundle.Instances) == 0 { - return fmt.Errorf("no instances found in bundle") - } - - for index := len(bundle.Instances) - 1; index >= 0; index-- { - instance := bundle.Instances[index] - log.Info(fmt.Sprintf("deleting instance %s", instance.Name)) - if err := deleteBundleInstance(ctx, instance, bundleDelArgs.wait, bundleDelArgs.dryrun); err != nil { - return err - } - } - return nil } @@ -208,7 +160,6 @@ func deleteBundleInstance(ctx context.Context, instance *engine.BundleInstance, return nil } - log.Info(fmt.Sprintf("deleting %v resource(s)...", len(objects))) hasErrors := false cs := ssa.NewChangeSet() for _, object := range objects { diff --git a/cmd/timoni/bundle_delete_test.go b/cmd/timoni/bundle_delete_test.go index f06aada7..f930f6f6 100644 --- a/cmd/timoni/bundle_delete_test.go +++ b/cmd/timoni/bundle_delete_test.go @@ -88,32 +88,7 @@ bundle: { err = envTestClient.Get(context.Background(), client.ObjectKeyFromObject(clientCM), clientCM) g.Expect(err).ToNot(HaveOccurred()) - output, err := executeCommandWithIn("bundle delete -f - --wait", strings.NewReader(bundleData)) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(output).To(ContainSubstring("frontend")) - g.Expect(output).To(ContainSubstring("backend")) - - err = envTestClient.Get(context.Background(), client.ObjectKeyFromObject(clientCM), clientCM) - g.Expect(errors.IsNotFound(err)).To(BeTrue()) - }) - - t.Run("deletes instances from named bundle", func(t *testing.T) { - g := NewWithT(t) - - _, err := executeCommandWithIn("bundle apply -f - -p main --wait", strings.NewReader(bundleData)) - g.Expect(err).ToNot(HaveOccurred()) - - clientCM := &corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "frontend-client", - Namespace: namespace, - }, - } - - err = envTestClient.Get(context.Background(), client.ObjectKeyFromObject(clientCM), clientCM) - g.Expect(err).ToNot(HaveOccurred()) - - output, err := executeCommand(fmt.Sprintf("bundle delete --name %[1]s --namespace %[2]s --wait", bundleName, namespace)) + output, err := executeCommand(fmt.Sprintf("bundle delete %s --wait", bundleName)) g.Expect(err).ToNot(HaveOccurred()) g.Expect(output).To(ContainSubstring("frontend")) g.Expect(output).To(ContainSubstring("backend")) @@ -193,7 +168,7 @@ bundle: { err = envTestClient.Get(context.Background(), client.ObjectKeyFromObject(serverCM), serverCM) g.Expect(err).ToNot(HaveOccurred()) - output, err := executeCommand(fmt.Sprintf("bundle delete --name %[1]s --wait", bundleName)) + output, err := executeCommand(fmt.Sprintf("bundle delete %s --wait", bundleName)) g.Expect(err).ToNot(HaveOccurred()) g.Expect(output).To(ContainSubstring("frontend")) g.Expect(output).To(ContainSubstring("backend")) diff --git a/docs/bundle.md b/docs/bundle.md index b6af194d..c83a4095 100644 --- a/docs/bundle.md +++ b/docs/bundle.md @@ -713,23 +713,26 @@ Timoni supports the following extensions: `.cue`, `.json`, `.yml`, `.yaml`. ### Uninstall -To uninstall the instances defined in a Bundle file, +To uninstall all the instances belonging to a Bundle, you can use the `timoni bundle delete` command. -Example: +Example using the bundle name: ```shell -timoni bundle delete -f bundle.cue +timoni bundle delete my-bundle ``` -Another option is to specify the Bundle name, and Timoni -will search the cluster and delete all the instances having -the `bundle.timoni.sh/name: ` label: +Example using a bundle CUE file: ```shell -timoni bundle delete --name my-bundle +timoni bundle delete -f bundle.cue ``` +Timoni will search the cluster and delete all the instances having +the `bundle.timoni.sh/name: ` label matching the given bundle name. +The instances are uninstalled in reverse order, +first created instance is last to be deleted. + ### Garbage collection Timoni's garbage collector keeps track of the applied resources and prunes the Kubernetes diff --git a/internal/engine/utils.go b/internal/engine/utils.go index 3b45707f..f9eaa87b 100644 --- a/internal/engine/utils.go +++ b/internal/engine/utils.go @@ -18,6 +18,7 @@ package engine import ( "bufio" + "fmt" "os" "path/filepath" "strings" @@ -105,6 +106,30 @@ func ExtractValueFromBytes(ctx *cue.Context, data []byte, expr string) (cue.Valu return value, nil } +func ExtractStringFromFile(ctx *cue.Context, filePath, exprPath string) (string, error) { + vData, err := os.ReadFile(filePath) + if err != nil { + return "", err + } + + value := ctx.CompileBytes(vData) + if value.Err() != nil { + return "", fmt.Errorf("compiling CUE file failed: %w", value.Err()) + } + + expr := value.LookupPath(cue.ParsePath(exprPath)) + if expr.Err() != nil { + return "", fmt.Errorf("lookup path failed: %w", expr.Err()) + } + + result, err := expr.String() + if expr.Err() != nil { + return "", fmt.Errorf("reading string failed: %w", expr.Err()) + } + + return result, nil +} + // MergeValue merges the given overlay on top of the base CUE value. // New fields from the overlay are added to the base and // existing fields are overridden with the overlay values.