diff --git a/cmd/flux/bootstrap.go b/cmd/flux/bootstrap.go index 2441551d..de02154a 100644 --- a/cmd/flux/bootstrap.go +++ b/cmd/flux/bootstrap.go @@ -17,11 +17,15 @@ limitations under the License. package main import ( + "context" "crypto/elliptic" "fmt" "strings" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" @@ -72,6 +76,8 @@ type bootstrapFlags struct { gpgPassphrase string gpgKeyID string + force bool + commitMessageAppendix string } @@ -129,6 +135,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, "override existing Flux installation if it's managed by a diffrent tool such as Helm") bootstrapCmd.PersistentFlags().MarkHidden("manifests") rootCmd.AddCommand(bootstrapCmd) @@ -188,3 +195,27 @@ func mapTeamSlice(s []string, defaultPermission string) map[string]string { return m } + +// confirmBootstrap gets a confirmation for running bootstrap over an existing Flux installation. +// It returns a nil error if Flux is not installed or the user confirms overriding an existing installation +func confirmBootstrap(ctx context.Context, kubeClient client.Client) error { + installed := true + info, err := getFluxClusterInfo(ctx, kubeClient) + if err != nil { + if !errors.IsNotFound(err) { + return fmt.Errorf("cluster info unavailable: %w", err) + } + installed = false + } + + if installed { + err = confirmFluxInstallOverride(info) + if err != nil { + if err == promptui.ErrAbort { + return fmt.Errorf("bootstrap cancelled") + } + return err + } + } + return nil +} diff --git a/cmd/flux/bootstrap_bitbucket_server.go b/cmd/flux/bootstrap_bitbucket_server.go index 40eccca8..3648f0d2 100644 --- a/cmd/flux/bootstrap_bitbucket_server.go +++ b/cmd/flux/bootstrap_bitbucket_server.go @@ -124,6 +124,13 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error { return err } + if !bootstrapArgs.force { + err = confirmBootstrap(ctx, kubeClient) + if err != nil { + 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 bd9dc80a..eb66b0ad 100644 --- a/cmd/flux/bootstrap_git.go +++ b/cmd/flux/bootstrap_git.go @@ -146,6 +146,13 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error { return err } + if !bootstrapArgs.force { + err = confirmBootstrap(ctx, kubeClient) + if err != nil { + 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 8c7c214e..c2860d05 100644 --- a/cmd/flux/bootstrap_github.go +++ b/cmd/flux/bootstrap_github.go @@ -128,6 +128,13 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error { return err } + if !bootstrapArgs.force { + err = confirmBootstrap(ctx, kubeClient) + if err != nil { + 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 6bb111c3..90e6e968 100644 --- a/cmd/flux/bootstrap_gitlab.go +++ b/cmd/flux/bootstrap_gitlab.go @@ -145,6 +145,13 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error { return err } + if !bootstrapArgs.force { + err = confirmBootstrap(ctx, kubeClient) + if err != nil { + 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 0a1f59bf..0640a025 100644 --- a/cmd/flux/cluster_info.go +++ b/cmd/flux/cluster_info.go @@ -20,8 +20,8 @@ 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" "sigs.k8s.io/controller-runtime/pkg/client" @@ -47,12 +47,13 @@ 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) { +// It returns the NotFound error from the underlying library if it was unable to find +// the GitRepository CRD and this can be used to check if Flux is installed. +func getFluxClusterInfo(ctx context.Context, c client.Client) (fluxClusterInfo, error) { var info fluxClusterInfo crdMetadata := &metav1.PartialObjectMetadata{ TypeMeta: metav1.TypeMeta{ @@ -64,10 +65,7 @@ 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, false, err + return info, err } info.version = crdMetadata.Labels["app.kubernetes.io/version"] @@ -80,8 +78,29 @@ func getFluxClusterInfo(ctx context.Context, c client.Client) (fluxClusterInfo, info.bootstrapped = true } + // the `app.kubernetes.io` label is not set by flux but might be set by other + // tools used to install Flux e.g Helm. 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 installation is managed by Flux + if 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 bce39be9..550ab165 100644 --- a/cmd/flux/cluster_info_test.go +++ b/cmd/flux/cluster_info_test.go @@ -24,6 +24,7 @@ import ( . "github.com/onsi/gomega" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -44,12 +45,11 @@ 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", + wantErr: true, }, { name: "CRD with kustomize-controller labels", @@ -58,7 +58,6 @@ 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, @@ -72,7 +71,6 @@ 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, @@ -85,7 +83,6 @@ 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", @@ -94,14 +91,13 @@ func Test_getFluxClusterInfo(t *testing.T) { { name: "CRD with no labels", labels: map[string]string{}, - wantBool: true, + wantInfo: fluxClusterInfo{}, }, { 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", }, @@ -120,12 +116,14 @@ 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()) + g.Expect(errors.IsNotFound(err)).To(BeTrue()) + } 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 6580c0e4..795cc1aa 100644 --- a/cmd/flux/install.go +++ b/cmd/flux/install.go @@ -23,7 +23,9 @@ import ( "path/filepath" "time" + "github.com/manifoldco/promptui" "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/errors" "github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/utils" @@ -72,6 +74,7 @@ type installFlags struct { tokenAuth bool clusterDomain string tolerationKeys []string + force bool } var installArgs = NewInstallFlags() @@ -98,6 +101,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, "override existing Flux installation if it's managed by a diffrent tool such as Helm") installCmd.Flags().MarkHidden("manifests") rootCmd.AddCommand(installCmd) @@ -188,13 +192,28 @@ func installCmdRun(cmd *cobra.Command, args []string) error { return err } - info, installed, err := getFluxClusterInfo(ctx, kubeClient) + installed := true + info, err := getFluxClusterInfo(ctx, kubeClient) if err != nil { - return fmt.Errorf("cluster info unavailable: %w", err) + if !errors.IsNotFound(err) { + return fmt.Errorf("cluster info unavailable: %w", err) + } + installed = false + } + + if info.bootstrapped { + return fmt.Errorf("this cluster has already been bootstrapped with Flux %s! Please use 'flux bootstrap' to upgrade", + info.version) } - 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 installed && !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))