1
0
mirror of synced 2026-02-19 23:56:56 +00:00

Merge pull request #4355 from somtochiama/safe-bootstrap

Confirm before overriding installation by another manager
This commit is contained in:
Stefan Prodan
2023-11-01 13:09:12 +02:00
committed by GitHub
9 changed files with 143 additions and 32 deletions

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
} }

View File

@@ -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))
}) })
} }

View File

@@ -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 {
return fmt.Errorf("cluster info unavailable: %w", err) if !errors.IsNotFound(err) {
return fmt.Errorf("cluster info unavailable: %w", err)
}
installed = false
} }
if installed && info.bootstrapped { if info.bootstrapped {
return fmt.Errorf("this cluster has already been bootstrapped with Flux %s! Please use 'flux bootstrap' to upgrade", info.version) 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)) applyOutput, err := utils.Apply(ctx, kubeconfigArgs, kubeclientOptions, tmpDir, filepath.Join(tmpDir, manifest.Path))

View File

@@ -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,16 +60,6 @@ func init() {
} }
func uninstallCmdRun(cmd *cobra.Command, args []string) error { func uninstallCmdRun(cmd *cobra.Command, args []string) error {
if !uninstallArgs.dryRun && !uninstallArgs.silent {
prompt := promptui.Prompt{
Label: "Are you sure you want to delete Flux and its custom resource definitions",
IsConfirm: true,
}
if _, err := prompt.Run(); err != nil {
return fmt.Errorf("aborting")
}
}
ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout) ctx, cancel := context.WithTimeout(context.Background(), rootArgs.timeout)
defer cancel() defer cancel()
@@ -77,6 +68,27 @@ func uninstallCmdRun(cmd *cobra.Command, args []string) error {
return err return err
} }
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{
Label: promptLabel,
IsConfirm: true,
}
if _, err := prompt.Run(); err != nil {
return fmt.Errorf("aborting")
}
}
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)