Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor timoni bundle delete #213

Merged
merged 1 commit into from
Oct 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/timoni/bundle_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
})
Expand Down
113 changes: 32 additions & 81 deletions cmd/timoni/bundle_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.'.
Expand All @@ -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 <name>'")
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
}
Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down
29 changes: 2 additions & 27 deletions cmd/timoni/bundle_delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -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"))
Expand Down
17 changes: 10 additions & 7 deletions docs/bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <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: <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
Expand Down
25 changes: 25 additions & 0 deletions internal/engine/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package engine

import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -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.
Expand Down