diff --git a/cmd/vclusterctl/cmd/platform/destroy.go b/cmd/vclusterctl/cmd/platform/destroy.go index d884d8600..e74f29ca0 100644 --- a/cmd/vclusterctl/cmd/platform/destroy.go +++ b/cmd/vclusterctl/cmd/platform/destroy.go @@ -4,8 +4,10 @@ import ( "context" "errors" "fmt" + "os" "github.com/loft-sh/log" + "github.com/loft-sh/log/survey" "github.com/loft-sh/vcluster/pkg/cli/destroy" "github.com/loft-sh/vcluster/pkg/cli/flags" "github.com/loft-sh/vcluster/pkg/cli/start" @@ -37,6 +39,9 @@ func NewDestroyCmd(globalFlags *flags.GlobalFlags) *cobra.Command { Destroys a vCluster Platform instance in your Kubernetes cluster. +IMPORTANT: This action is done against the cluster the the kube-context is pointing to, and not the vCluster Platform instance that is logged in. +It does not require logging in to vCluster Platform. + Please make sure you meet the following requirements before running this command: @@ -59,6 +64,7 @@ VirtualClusterInstances managed with driver helm will be deleted, but the underl destroyCmd.Flags().BoolVar(&cmd.DeleteNamespace, "delete-namespace", true, "Whether to delete the namespace or not") destroyCmd.Flags().BoolVar(&cmd.IgnoreNotFound, "ignore-not-found", false, "Exit successfully if platform installation is not found") destroyCmd.Flags().BoolVar(&cmd.Force, "force", false, "Try uninstalling even if the platform is not installed. '--namespace' is required if true") + destroyCmd.Flags().BoolVar(&cmd.NonInteractive, "non-interactive", false, "Will not prompt for confirmation") destroyCmd.Flags().IntVar(&cmd.TimeoutMinutes, "timeout-minutes", 5, "How long to try deleting the platform before giving up") return destroyCmd @@ -66,7 +72,7 @@ VirtualClusterInstances managed with driver helm will be deleted, but the underl func (cmd *DestroyCmd) Run(ctx context.Context) error { // initialise clients, verify binaries exist, sanity-check context - err := cmd.Options.Prepare() + err := cmd.Options.Prepare(cmd.NonInteractive) if err != nil { return fmt.Errorf("failed to prepare clients: %w", err) } @@ -97,9 +103,35 @@ func (cmd *DestroyCmd) Run(ctx context.Context) error { return fmt.Errorf("platform not installed in namespace %q", cmd.Namespace) } + if !cmd.NonInteractive { + deleteOpt := "delete" + out, err := cmd.Log.Question(&survey.QuestionOptions{ + Question: fmt.Sprintf("IMPORTANT! You are destroy the vCluster Platform in the namespace %q.\nThis may result in data loss. Please ensure your kube-context is pointed at the right cluster.\n Please type %q to continue:", cmd.Namespace, deleteOpt), + }) + if err != nil { + return fmt.Errorf("failed to prompt for confirmation: %w", err) + } + if out != deleteOpt { + cmd.Log.Info("destroy cancelled") + return nil + } + } + err = destroy.Destroy(ctx, cmd.DeleteOptions) if err != nil { return fmt.Errorf("failed to destroy platform: %w", err) } + + cmd.Log.Infof("deleting platform config at %q", cmd.Config) + cliConfig := cmd.LoadedConfig(cmd.Log) + err = cliConfig.Delete() + if err != nil { + if errors.Is(err, os.ErrNotExist) && cmd.IgnoreNotFound { + cmd.Log.Info("no platform config detected") + return nil + } + return fmt.Errorf("failed to delete platform config: %w", err) + } + return nil } diff --git a/pkg/cli/config/config.go b/pkg/cli/config/config.go index 9593015bf..fafb4a940 100644 --- a/pkg/cli/config/config.go +++ b/pkg/cli/config/config.go @@ -1,7 +1,9 @@ package config import ( + "bytes" "encoding/json" + "errors" "fmt" "io" "os" @@ -53,6 +55,45 @@ func (c *CLI) Save() error { return Write(path, c) } +func (c *CLI) Delete() error { + if c == nil || c.path == "" { + return errors.New("nil config path") + } + + file, err := os.Open(c.path) + if err != nil { + return fmt.Errorf("failed to load vcluster configuration file from %q : %w", c.path, err) + } + stat, err := file.Stat() + if err != nil { + return fmt.Errorf("failed to load vcluster configuration file from %q: %w", c.path, err) + } + if stat.IsDir() { + return fmt.Errorf("failed to load vcluster configuration file %q", c.path) + } + defer file.Close() + + fileBytes, err := io.ReadAll(file) + if err != nil { + return fmt.Errorf("read all: %w", err) + } + + decoder := json.NewDecoder(bytes.NewReader(fileBytes)) + decoder.DisallowUnknownFields() + tryRead := &CLI{} + err = decoder.Decode(tryRead) + if err != nil { + return fmt.Errorf("failed to unmarshall vcluster configuration from %q: %w", c.path, err) + } + + // delete file at path + err = os.Remove(c.path) + if err != nil { + return fmt.Errorf("failed to delete configuration file at %q: %w", c.path, err) + } + return nil +} + // Read returns the current config by trying to read it from the given config path. // It returns a new default config if there have been any errors during the read. func Read(path string, log log.Logger) *CLI { diff --git a/pkg/cli/destroy/destroy.go b/pkg/cli/destroy/destroy.go index 639b1f6ea..65c6f4f4e 100644 --- a/pkg/cli/destroy/destroy.go +++ b/pkg/cli/destroy/destroy.go @@ -73,6 +73,7 @@ type DeleteOptions struct { DeleteNamespace bool IgnoreNotFound bool Force bool + NonInteractive bool TimeoutMinutes int } @@ -152,22 +153,6 @@ func Destroy(ctx context.Context, opts DeleteOptions) error { return err } - for _, name := range clihelper.DefaultClusterRoles { - name := name + "-binding" - opts.Log.Infof("deleting clusterrolebinding %q", name) - err := opts.KubeClient.RbacV1().ClusterRoleBindings().Delete(ctx, name+"-binding", metav1.DeleteOptions{}) - if err != nil && !kerrors.IsNotFound(err) { - return fmt.Errorf("failed to delete clusterrole: %w", err) - } - } - for _, name := range clihelper.DefaultClusterRoles { - opts.Log.Infof("deleting clusterrole %q", name) - err := opts.KubeClient.RbacV1().ClusterRoles().Delete(ctx, name, metav1.DeleteOptions{}) - if err != nil && !kerrors.IsNotFound(err) { - return fmt.Errorf("failed to delete clusterrole: %w", err) - } - } - opts.Log.Info("deleting CRDS") err = wait.ExponentialBackoffWithContext(ctx, wait.Backoff{Duration: time.Second, Factor: backoffFactor, Cap: time.Duration(opts.TimeoutMinutes) * time.Minute, Steps: math.MaxInt32}, func(ctx context.Context) (bool, error) { list, err := apiextensionclientset.ApiextensionsV1().CustomResourceDefinitions().List(ctx, metav1.ListOptions{}) @@ -236,6 +221,22 @@ func Destroy(ctx context.Context, opts DeleteOptions) error { } } + for _, name := range clihelper.DefaultClusterRoles { + name := name + "-binding" + opts.Log.Infof("deleting clusterrolebinding %q", name) + err := opts.KubeClient.RbacV1().ClusterRoleBindings().Delete(ctx, name, metav1.DeleteOptions{}) + if err != nil && !kerrors.IsNotFound(err) { + return fmt.Errorf("failed to delete clusterrole: %w", err) + } + } + for _, name := range clihelper.DefaultClusterRoles { + opts.Log.Infof("deleting clusterrole %q", name) + err := opts.KubeClient.RbacV1().ClusterRoles().Delete(ctx, name, metav1.DeleteOptions{}) + if err != nil && !kerrors.IsNotFound(err) { + return fmt.Errorf("failed to delete clusterrole: %w", err) + } + } + return nil } diff --git a/pkg/cli/start/start.go b/pkg/cli/start/start.go index 799e58865..184379a52 100644 --- a/pkg/cli/start/start.go +++ b/pkg/cli/start/start.go @@ -93,7 +93,7 @@ func (l *LoftStarter) Start(ctx context.Context) error { l.LocalPort = "9898" } - err := l.Prepare() + err := l.Prepare(false) if err != nil { return err } @@ -141,7 +141,7 @@ func (l *LoftStarter) Start(ctx context.Context) error { } // Prepare initializes clients, verifies the existense of binaries, and ensures we are starting with the right kube context -func (l *Options) Prepare() error { +func (l *Options) Prepare(cliNonInteractive bool) error { platformClient := platform.NewClientFromConfig(l.LoadedConfig(l.Log)) platformConfig := platformClient.Config().Platform @@ -160,15 +160,18 @@ func (l *Options) Prepare() error { if l.Context != "" { contextToLoad = l.Context } else if platformConfig.LastInstallContext != "" && platformConfig.LastInstallContext != contextToLoad { - contextToLoad, err = l.Log.Question(&survey.QuestionOptions{ - Question: product.Replace(fmt.Sprintf("Seems like you try to use 'vcluster %s' with a different kubernetes context than before. Please choose which kubernetes context you want to use", l.CommandName)), - DefaultValue: contextToLoad, - Options: []string{contextToLoad, platformConfig.LastInstallContext}, - }) - if err != nil { - return err + if !cliNonInteractive { + contextToLoad, err = l.Log.Question(&survey.QuestionOptions{ + Question: product.Replace(fmt.Sprintf("Seems like you try to use 'vcluster %s' with a different kubernetes context than before. Please choose which kubernetes context you want to use", l.CommandName)), + DefaultValue: contextToLoad, + Options: []string{contextToLoad, platformConfig.LastInstallContext}, + }) + if err != nil { + return err + } } } + l.Context = contextToLoad platformConfig.LastInstallContext = contextToLoad diff --git a/pkg/platform/client.go b/pkg/platform/client.go index 92406f14b..2cba31484 100644 --- a/pkg/platform/client.go +++ b/pkg/platform/client.go @@ -157,6 +157,10 @@ func (c *client) Save() error { return c.config.Save() } +func (c *client) Delete() error { + return c.config.Delete() +} + func (c *client) ManagementConfig() (*rest.Config, error) { return c.restConfig("/kubernetes/management") } diff --git a/pkg/platform/clihelper/clihelper.go b/pkg/platform/clihelper/clihelper.go index 222b844de..b79e338bf 100644 --- a/pkg/platform/clihelper/clihelper.go +++ b/pkg/platform/clihelper/clihelper.go @@ -67,8 +67,10 @@ var defaultDeploymentName = "loft" var DefaultClusterRoles = []string{ "loft-agent-cluster", - "loft-runnner-cluster", + "loft-runner-cluster", "loft-vcluster-cluster", + "loft-cluster-authenticated", + "loft-management-authenticated", } func Timeout() time.Duration {