diff --git a/cmd/flux/bootstrap.go b/cmd/flux/bootstrap.go index 2441551df7..467b5f0b65 100644 --- a/cmd/flux/bootstrap.go +++ b/cmd/flux/bootstrap.go @@ -72,6 +72,8 @@ type bootstrapFlags struct { gpgPassphrase string gpgKeyID string + force bool + commitMessageAppendix string } @@ -129,6 +131,7 @@ func init() { bootstrapCmd.PersistentFlags().StringVar(&bootstrapArgs.commitMessageAppendix, "commit-message-appendix", "", "string to add to the commit messages, e.g. '[ci skip]'") + bootstrapCmd.PersistentFlags().BoolVar(&bootstrapArgs.force, "force", false, "overwrite existing Flux installation on the cluster") bootstrapCmd.PersistentFlags().MarkHidden("manifests") rootCmd.AddCommand(bootstrapCmd) diff --git a/cmd/flux/bootstrap_bitbucket_server.go b/cmd/flux/bootstrap_bitbucket_server.go index 40eccca88c..fe174bb99a 100644 --- a/cmd/flux/bootstrap_bitbucket_server.go +++ b/cmd/flux/bootstrap_bitbucket_server.go @@ -24,6 +24,7 @@ import ( "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/gogit" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" "github.com/fluxcd/flux2/v2/internal/flags" @@ -124,6 +125,21 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error { return err } + if !bootstrapArgs.force { + info, err := getFluxClusterInfo(ctx, kubeClient) + if err != nil { + return fmt.Errorf("cluster info unavailable: %w", err) + } + + err = confirmFluxInstallOverride(info) + if err != nil { + if err == promptui.ErrAbort { + return fmt.Errorf("bootstrap cancelled") + } + return err + } + } + // Manifest base if ver, err := getVersion(bootstrapArgs.version); err != nil { return err diff --git a/cmd/flux/bootstrap_git.go b/cmd/flux/bootstrap_git.go index bd9dc80aa2..db2c746dee 100644 --- a/cmd/flux/bootstrap_git.go +++ b/cmd/flux/bootstrap_git.go @@ -146,6 +146,21 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { return err } + if !bootstrapArgs.force { + info, err := getFluxClusterInfo(ctx, kubeClient) + if err != nil { + return fmt.Errorf("cluster info unavailable: %w", err) + } + + err = confirmFluxInstallOverride(info) + if err != nil { + if err == promptui.ErrAbort { + return fmt.Errorf("bootstrap cancelled") + } + return err + } + } + // Manifest base if ver, err := getVersion(bootstrapArgs.version); err != nil { return err diff --git a/cmd/flux/bootstrap_github.go b/cmd/flux/bootstrap_github.go index 8c7c214ee5..d468729854 100644 --- a/cmd/flux/bootstrap_github.go +++ b/cmd/flux/bootstrap_github.go @@ -24,6 +24,7 @@ import ( "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/gogit" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" "github.com/fluxcd/flux2/v2/internal/flags" @@ -128,6 +129,20 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { return err } + if !bootstrapArgs.force { + info, err := getFluxClusterInfo(ctx, kubeClient) + if err != nil { + return fmt.Errorf("cluster info unavailable: %w", err) + } + err = confirmFluxInstallOverride(info) + if err != nil { + if err == promptui.ErrAbort { + return fmt.Errorf("bootstrap cancelled") + } + return err + } + } + // Manifest base if ver, err := getVersion(bootstrapArgs.version); err != nil { return err diff --git a/cmd/flux/bootstrap_gitlab.go b/cmd/flux/bootstrap_gitlab.go index 6bb111c37f..0f860b61ec 100644 --- a/cmd/flux/bootstrap_gitlab.go +++ b/cmd/flux/bootstrap_gitlab.go @@ -26,6 +26,7 @@ import ( "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/git/gogit" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" "github.com/fluxcd/flux2/v2/internal/flags" @@ -145,6 +146,21 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { return err } + if !bootstrapArgs.force { + info, err := getFluxClusterInfo(ctx, kubeClient) + if err != nil { + return fmt.Errorf("cluster info unavailable: %w", err) + } + + err = confirmFluxInstallOverride(info) + if err != nil { + if err == promptui.ErrAbort { + return fmt.Errorf("bootstrap cancelled") + } + return err + } + } + // Manifest base if ver, err := getVersion(bootstrapArgs.version); err != nil { return err diff --git a/cmd/flux/cluster_info.go b/cmd/flux/cluster_info.go index 0a1f59bfdd..7cd91ef75d 100644 --- a/cmd/flux/cluster_info.go +++ b/cmd/flux/cluster_info.go @@ -20,6 +20,7 @@ import ( "context" "fmt" + "github.com/manifoldco/promptui" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -38,6 +39,8 @@ var bootstrapLabels = []string{ // fluxClusterInfo contains information about an existing flux installation on a cluster. type fluxClusterInfo struct { + // installed indicates that Flux is installed on the cluster. + installed bool // bootstrapped indicates that Flux was installed using the `flux bootstrap` command. bootstrapped bool // managedBy is the name of the tool being used to manage the installation of Flux. @@ -47,12 +50,11 @@ type fluxClusterInfo struct { } // getFluxClusterInfo returns information on the Flux installation running on the cluster. -// If the information cannot be retrieved, the boolean return value will be false. // If an error occurred, the returned error will be non-nil. // // This function retrieves the GitRepository CRD from the cluster and checks it // for a set of labels used to determine the Flux version and how Flux was installed. -func getFluxClusterInfo(ctx context.Context, c client.Client) (fluxClusterInfo, bool, error) { +func getFluxClusterInfo(ctx context.Context, c client.Client) (fluxClusterInfo, error) { var info fluxClusterInfo crdMetadata := &metav1.PartialObjectMetadata{ TypeMeta: metav1.TypeMeta{ @@ -65,11 +67,12 @@ func getFluxClusterInfo(ctx context.Context, c client.Client) (fluxClusterInfo, } if err := c.Get(ctx, client.ObjectKeyFromObject(crdMetadata), crdMetadata); err != nil { if errors.IsNotFound(err) { - return info, false, nil + return info, nil } - return info, false, err + return info, err } + info.installed = true info.version = crdMetadata.Labels["app.kubernetes.io/version"] var present bool @@ -83,5 +86,25 @@ func getFluxClusterInfo(ctx context.Context, c client.Client) (fluxClusterInfo, if manager, ok := crdMetadata.Labels["app.kubernetes.io/managed-by"]; ok { info.managedBy = manager } - return info, true, nil + return info, nil +} + +// confirmFluxInstallOverride displays a prompt to the user so that they can confirm before overriding +// a Flux installation. It returns nil if the installation should continue, +// promptui.ErrAbort if the user doesn't confirm, or an error encountered. +func confirmFluxInstallOverride(info fluxClusterInfo) error { + // no need to display prompt if flux is not installed or installation is + // managed by Flux + if !info.installed || info.managedBy == "" || info.managedBy == "flux" { + return nil + } + + display := fmt.Sprintf("Flux %s has been installed on this cluster with %s!", info.version, info.managedBy) + fmt.Fprintln(rootCmd.ErrOrStderr(), display) + prompt := promptui.Prompt{ + Label: fmt.Sprintf("Are you sure you want to override the %s installation? Y/N", info.managedBy), + IsConfirm: true, + } + _, err := prompt.Run() + return err } diff --git a/cmd/flux/cluster_info_test.go b/cmd/flux/cluster_info_test.go index bce39be9b9..d4c7ab27ed 100644 --- a/cmd/flux/cluster_info_test.go +++ b/cmd/flux/cluster_info_test.go @@ -44,12 +44,10 @@ func Test_getFluxClusterInfo(t *testing.T) { name string labels map[string]string wantErr bool - wantBool bool wantInfo fluxClusterInfo }{ { - name: "no git repository CRD present", - wantBool: false, + name: "no git repository CRD present", }, { name: "CRD with kustomize-controller labels", @@ -58,10 +56,10 @@ func Test_getFluxClusterInfo(t *testing.T) { fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group): "flux-system", "app.kubernetes.io/version": "v2.1.0", }, - wantBool: true, wantInfo: fluxClusterInfo{ version: "v2.1.0", bootstrapped: true, + installed: true, }, }, { @@ -72,10 +70,10 @@ func Test_getFluxClusterInfo(t *testing.T) { "app.kubernetes.io/version": "v2.1.0", "app.kubernetes.io/managed-by": "flux", }, - wantBool: true, wantInfo: fluxClusterInfo{ version: "v2.1.0", bootstrapped: true, + installed: true, managedBy: "flux", }, }, @@ -85,25 +83,27 @@ func Test_getFluxClusterInfo(t *testing.T) { "app.kubernetes.io/version": "v2.1.0", "app.kubernetes.io/managed-by": "helm", }, - wantBool: true, wantInfo: fluxClusterInfo{ version: "v2.1.0", managedBy: "helm", + installed: true, }, }, { - name: "CRD with no labels", - labels: map[string]string{}, - wantBool: true, + name: "CRD with no labels", + labels: map[string]string{}, + wantInfo: fluxClusterInfo{ + installed: true, + }, }, { name: "CRD with only version label", labels: map[string]string{ "app.kubernetes.io/version": "v2.1.0", }, - wantBool: true, wantInfo: fluxClusterInfo{ - version: "v2.1.0", + version: "v2.1.0", + installed: true, }, }, } @@ -120,12 +120,13 @@ func Test_getFluxClusterInfo(t *testing.T) { } client := builder.Build() - info, present, err := getFluxClusterInfo(context.Background(), client) + info, err := getFluxClusterInfo(context.Background(), client) if tt.wantErr { g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).To(Not(HaveOccurred())) } - g.Expect(present).To(Equal(tt.wantBool)) g.Expect(info).To(BeEquivalentTo(tt.wantInfo)) }) } diff --git a/cmd/flux/install.go b/cmd/flux/install.go index 6580c0e4d5..809a961635 100644 --- a/cmd/flux/install.go +++ b/cmd/flux/install.go @@ -23,6 +23,7 @@ import ( "path/filepath" "time" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" "github.com/fluxcd/flux2/v2/internal/flags" @@ -72,6 +73,7 @@ type installFlags struct { tokenAuth bool clusterDomain string tolerationKeys []string + force bool } var installArgs = NewInstallFlags() @@ -98,6 +100,7 @@ func init() { installCmd.Flags().StringVar(&installArgs.clusterDomain, "cluster-domain", rootArgs.defaults.ClusterDomain, "internal cluster domain") installCmd.Flags().StringSliceVar(&installArgs.tolerationKeys, "toleration-keys", nil, "list of toleration keys used to schedule the components pods onto nodes with matching taints") + installCmd.Flags().BoolVar(&installArgs.force, "force", false, "overwrite existing Flux installation on the cluster") installCmd.Flags().MarkHidden("manifests") rootCmd.AddCommand(installCmd) @@ -188,13 +191,24 @@ func installCmdRun(cmd *cobra.Command, args []string) error { return err } - info, installed, err := getFluxClusterInfo(ctx, kubeClient) + info, err := getFluxClusterInfo(ctx, kubeClient) if err != nil { return fmt.Errorf("cluster info unavailable: %w", err) } - if installed && info.bootstrapped { - return fmt.Errorf("this cluster has already been bootstrapped with Flux %s! Please use 'flux bootstrap' to upgrade", info.version) + if info.installed && info.bootstrapped { + return fmt.Errorf("this cluster has already been bootstrapped with Flux %s! Please use 'flux bootstrap' to upgrade", + info.version) + } + + if !installArgs.force { + err := confirmFluxInstallOverride(info) + if err != nil { + if err == promptui.ErrAbort { + return fmt.Errorf("installation cancelled") + } + return err + } } applyOutput, err := utils.Apply(ctx, kubeconfigArgs, kubeclientOptions, tmpDir, filepath.Join(tmpDir, manifest.Path))