Merge pull request #4355 from somtochiama/safe-bootstrap

Confirm before overriding installation by another manager
pull/4270/head
Stefan Prodan 1 year ago committed by GitHub
commit 28971edc07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -17,11 +17,15 @@ limitations under the License.
package main package main
import ( import (
"context"
"crypto/elliptic" "crypto/elliptic"
"fmt" "fmt"
"strings" "strings"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra" "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/flags"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
@ -72,6 +76,8 @@ type bootstrapFlags struct {
gpgPassphrase string gpgPassphrase string
gpgKeyID string gpgKeyID string
force bool
commitMessageAppendix string 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().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") bootstrapCmd.PersistentFlags().MarkHidden("manifests")
rootCmd.AddCommand(bootstrapCmd) rootCmd.AddCommand(bootstrapCmd)
@ -188,3 +195,27 @@ func mapTeamSlice(s []string, defaultPermission string) map[string]string {
return m 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
}

@ -124,6 +124,13 @@ func bootstrapBServerCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
if !bootstrapArgs.force {
err = confirmBootstrap(ctx, kubeClient)
if err != nil {
return err
}
}
// Manifest base // Manifest base
if ver, err := getVersion(bootstrapArgs.version); err != nil { if ver, err := getVersion(bootstrapArgs.version); err != nil {
return err return err

@ -146,6 +146,13 @@ func bootstrapGitCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
if !bootstrapArgs.force {
err = confirmBootstrap(ctx, kubeClient)
if err != nil {
return err
}
}
// Manifest base // Manifest base
if ver, err := getVersion(bootstrapArgs.version); err != nil { if ver, err := getVersion(bootstrapArgs.version); err != nil {
return err return err

@ -128,6 +128,13 @@ func bootstrapGitHubCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
if !bootstrapArgs.force {
err = confirmBootstrap(ctx, kubeClient)
if err != nil {
return err
}
}
// Manifest base // Manifest base
if ver, err := getVersion(bootstrapArgs.version); err != nil { if ver, err := getVersion(bootstrapArgs.version); err != nil {
return err return err

@ -145,6 +145,13 @@ func bootstrapGitLabCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
if !bootstrapArgs.force {
err = confirmBootstrap(ctx, kubeClient)
if err != nil {
return err
}
}
// Manifest base // Manifest base
if ver, err := getVersion(bootstrapArgs.version); err != nil { if ver, err := getVersion(bootstrapArgs.version); err != nil {
return err return err

@ -20,8 +20,8 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/manifoldco/promptui"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client" "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. // 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. // If an error occurred, the returned error will be non-nil.
// //
// This function retrieves the GitRepository CRD from the cluster and checks it // 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. // 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 var info fluxClusterInfo
crdMetadata := &metav1.PartialObjectMetadata{ crdMetadata := &metav1.PartialObjectMetadata{
TypeMeta: metav1.TypeMeta{ 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 err := c.Get(ctx, client.ObjectKeyFromObject(crdMetadata), crdMetadata); err != nil {
if errors.IsNotFound(err) { return info, err
return info, false, nil
}
return info, false, err
} }
info.version = crdMetadata.Labels["app.kubernetes.io/version"] info.version = crdMetadata.Labels["app.kubernetes.io/version"]
@ -80,8 +78,33 @@ func getFluxClusterInfo(ctx context.Context, c client.Client) (fluxClusterInfo,
info.bootstrapped = true 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 { if manager, ok := crdMetadata.Labels["app.kubernetes.io/managed-by"]; ok {
info.managedBy = manager 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 installManagedByFlux(info.managedBy) {
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
}
func installManagedByFlux(manager string) bool {
return manager == "" || manager == "flux"
} }

@ -24,6 +24,7 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/client/fake"
@ -44,12 +45,11 @@ func Test_getFluxClusterInfo(t *testing.T) {
name string name string
labels map[string]string labels map[string]string
wantErr bool wantErr bool
wantBool bool
wantInfo fluxClusterInfo wantInfo fluxClusterInfo
}{ }{
{ {
name: "no git repository CRD present", name: "no git repository CRD present",
wantBool: false, wantErr: true,
}, },
{ {
name: "CRD with kustomize-controller labels", 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", fmt.Sprintf("%s/namespace", kustomizev1.GroupVersion.Group): "flux-system",
"app.kubernetes.io/version": "v2.1.0", "app.kubernetes.io/version": "v2.1.0",
}, },
wantBool: true,
wantInfo: fluxClusterInfo{ wantInfo: fluxClusterInfo{
version: "v2.1.0", version: "v2.1.0",
bootstrapped: true, bootstrapped: true,
@ -72,7 +71,6 @@ func Test_getFluxClusterInfo(t *testing.T) {
"app.kubernetes.io/version": "v2.1.0", "app.kubernetes.io/version": "v2.1.0",
"app.kubernetes.io/managed-by": "flux", "app.kubernetes.io/managed-by": "flux",
}, },
wantBool: true,
wantInfo: fluxClusterInfo{ wantInfo: fluxClusterInfo{
version: "v2.1.0", version: "v2.1.0",
bootstrapped: true, bootstrapped: true,
@ -85,7 +83,6 @@ func Test_getFluxClusterInfo(t *testing.T) {
"app.kubernetes.io/version": "v2.1.0", "app.kubernetes.io/version": "v2.1.0",
"app.kubernetes.io/managed-by": "helm", "app.kubernetes.io/managed-by": "helm",
}, },
wantBool: true,
wantInfo: fluxClusterInfo{ wantInfo: fluxClusterInfo{
version: "v2.1.0", version: "v2.1.0",
managedBy: "helm", managedBy: "helm",
@ -94,14 +91,13 @@ func Test_getFluxClusterInfo(t *testing.T) {
{ {
name: "CRD with no labels", name: "CRD with no labels",
labels: map[string]string{}, labels: map[string]string{},
wantBool: true, wantInfo: fluxClusterInfo{},
}, },
{ {
name: "CRD with only version label", name: "CRD with only version label",
labels: map[string]string{ labels: map[string]string{
"app.kubernetes.io/version": "v2.1.0", "app.kubernetes.io/version": "v2.1.0",
}, },
wantBool: true,
wantInfo: fluxClusterInfo{ wantInfo: fluxClusterInfo{
version: "v2.1.0", version: "v2.1.0",
}, },
@ -120,12 +116,14 @@ func Test_getFluxClusterInfo(t *testing.T) {
} }
client := builder.Build() client := builder.Build()
info, present, err := getFluxClusterInfo(context.Background(), client) info, err := getFluxClusterInfo(context.Background(), client)
if tt.wantErr { if tt.wantErr {
g.Expect(err).To(HaveOccurred()) 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)) g.Expect(info).To(BeEquivalentTo(tt.wantInfo))
}) })
} }

@ -23,7 +23,9 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/manifoldco/promptui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
"github.com/fluxcd/flux2/v2/internal/flags" "github.com/fluxcd/flux2/v2/internal/flags"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
@ -72,6 +74,7 @@ type installFlags struct {
tokenAuth bool tokenAuth bool
clusterDomain string clusterDomain string
tolerationKeys []string tolerationKeys []string
force bool
} }
var installArgs = NewInstallFlags() var installArgs = NewInstallFlags()
@ -98,6 +101,7 @@ func init() {
installCmd.Flags().StringVar(&installArgs.clusterDomain, "cluster-domain", rootArgs.defaults.ClusterDomain, "internal cluster domain") installCmd.Flags().StringVar(&installArgs.clusterDomain, "cluster-domain", rootArgs.defaults.ClusterDomain, "internal cluster domain")
installCmd.Flags().StringSliceVar(&installArgs.tolerationKeys, "toleration-keys", nil, installCmd.Flags().StringSliceVar(&installArgs.tolerationKeys, "toleration-keys", nil,
"list of toleration keys used to schedule the components pods onto nodes with matching taints") "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") installCmd.Flags().MarkHidden("manifests")
rootCmd.AddCommand(installCmd) rootCmd.AddCommand(installCmd)
@ -188,13 +192,28 @@ func installCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
info, installed, err := getFluxClusterInfo(ctx, kubeClient) installed := true
info, err := getFluxClusterInfo(ctx, kubeClient)
if err != nil { if err != nil {
if !errors.IsNotFound(err) {
return fmt.Errorf("cluster info unavailable: %w", 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 { if installed && !installArgs.force {
return fmt.Errorf("this cluster has already been bootstrapped with Flux %s! Please use 'flux bootstrap' to upgrade", info.version) 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)) applyOutput, err := utils.Apply(ctx, kubeconfigArgs, kubeclientOptions, tmpDir, filepath.Join(tmpDir, manifest.Path))

@ -22,6 +22,7 @@ import (
"github.com/manifoldco/promptui" "github.com/manifoldco/promptui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
"github.com/fluxcd/flux2/v2/internal/utils" "github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/uninstall" "github.com/fluxcd/flux2/v2/pkg/uninstall"
@ -59,9 +60,28 @@ func init() {
} }
func uninstallCmdRun(cmd *cobra.Command, args []string) error { func uninstallCmdRun(cmd *cobra.Command, args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}
if !uninstallArgs.dryRun && !uninstallArgs.silent { if !uninstallArgs.dryRun && !uninstallArgs.silent {
info, err := getFluxClusterInfo(ctx, kubeClient)
if err != nil {
if !errors.IsNotFound(err) {
return fmt.Errorf("cluster info unavailable: %w", err)
}
}
promptLabel := "Are you sure you want to delete Flux and its custom resource definitions"
if !installManagedByFlux(info.managedBy) {
promptLabel = fmt.Sprintf("Flux is managed by %s! Are you sure you want to delete Flux and its CRDs using Flux CLI", info.managedBy)
}
prompt := promptui.Prompt{ prompt := promptui.Prompt{
Label: "Are you sure you want to delete Flux and its custom resource definitions", Label: promptLabel,
IsConfirm: true, IsConfirm: true,
} }
if _, err := prompt.Run(); err != nil { if _, err := prompt.Run(); err != nil {
@ -69,14 +89,6 @@ func uninstallCmdRun(cmd *cobra.Command, args []string) error {
} }
} }
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel()
kubeClient, err := utils.KubeClient(kubeconfigArgs, kubeclientOptions)
if err != nil {
return err
}
logger.Actionf("deleting components in %s namespace", *kubeconfigArgs.Namespace) logger.Actionf("deleting components in %s namespace", *kubeconfigArgs.Namespace)
uninstall.Components(ctx, logger, kubeClient, *kubeconfigArgs.Namespace, uninstallArgs.dryRun) uninstall.Components(ctx, logger, kubeClient, *kubeconfigArgs.Namespace, uninstallArgs.dryRun)

Loading…
Cancel
Save