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

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

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

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

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

@ -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,33 @@ 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 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"
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))
})
}

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

@ -22,6 +22,7 @@ import (
"github.com/manifoldco/promptui"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/api/errors"
"github.com/fluxcd/flux2/v2/internal/utils"
"github.com/fluxcd/flux2/v2/pkg/uninstall"
@ -59,9 +60,28 @@ func init() {
}
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 {
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: "Are you sure you want to delete Flux and its custom resource definitions",
Label: promptLabel,
IsConfirm: true,
}
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)
uninstall.Components(ctx, logger, kubeClient, *kubeconfigArgs.Namespace, uninstallArgs.dryRun)

Loading…
Cancel
Save