mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
cli: ask user to confirm cert-manager upgrades
This commit is contained in:
parent
e7c7e35f51
commit
075a0e0ad6
@ -109,8 +109,8 @@ func (u *Upgrader) GetCurrentImage(ctx context.Context) (*unstructured.Unstructu
|
||||
}
|
||||
|
||||
// UpgradeHelmServices upgrade helm services.
|
||||
func (u *Upgrader) UpgradeHelmServices(ctx context.Context, config *config.Config, timeout time.Duration) error {
|
||||
return u.helmClient.Upgrade(ctx, config, timeout)
|
||||
func (u *Upgrader) UpgradeHelmServices(ctx context.Context, config *config.Config, timeout time.Duration, allowDestructive bool) error {
|
||||
return u.helmClient.Upgrade(ctx, config, timeout, allowDestructive)
|
||||
}
|
||||
|
||||
// KubernetesVersion returns the version of Kubernetes the Constellation is currently running on.
|
||||
@ -234,7 +234,7 @@ func (u *stableClient) kubernetesVersion() (string, error) {
|
||||
}
|
||||
|
||||
type helmInterface interface {
|
||||
Upgrade(ctx context.Context, config *config.Config, timeout time.Duration) error
|
||||
Upgrade(ctx context.Context, config *config.Config, timeout time.Duration, allowDestructive bool) error
|
||||
}
|
||||
|
||||
type debugLog interface {
|
||||
|
@ -8,10 +8,12 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/helm"
|
||||
"github.com/edgelesssys/constellation/v2/cli/internal/image"
|
||||
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
|
||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||
@ -30,6 +32,7 @@ func newUpgradeExecuteCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
cmd.Flags().Bool("helm", false, "Execute helm upgrade. This feature is still in development an may change without anounncement. Upgrades all helm charts deployed during constellation-init.")
|
||||
cmd.Flags().BoolP("yes", "y", false, "Run upgrades without further confirmation. WARNING: might delete your resources in case you are using cert-manager in your cluster. Please read the docs.")
|
||||
cmd.Flags().Duration("timeout", 3*time.Minute, "Change helm upgrade timeout. This feature is still in development an may change without anounncement. Might be useful for slow connections or big clusters.")
|
||||
if err := cmd.Flags().MarkHidden("helm"); err != nil {
|
||||
panic(err)
|
||||
@ -59,27 +62,35 @@ func runUpgradeExecute(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
|
||||
func upgradeExecute(cmd *cobra.Command, imageFetcher imageFetcher, upgrader cloudUpgrader, fileHandler file.Handler) error {
|
||||
configPath, err := cmd.Flags().GetString("config")
|
||||
flags, err := parseUpgradeExecuteFlags(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("parsing flags: %w", err)
|
||||
}
|
||||
conf, err := config.New(fileHandler, configPath)
|
||||
conf, err := config.New(fileHandler, flags.configPath)
|
||||
if err != nil {
|
||||
return displayConfigValidationErrors(cmd.ErrOrStderr(), err)
|
||||
}
|
||||
|
||||
helm, err := cmd.Flags().GetBool("helm")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if helm {
|
||||
timeout, err := cmd.Flags().GetDuration("timeout")
|
||||
if err != nil {
|
||||
return err
|
||||
if flags.helmFlag {
|
||||
err = upgrader.UpgradeHelmServices(cmd.Context(), conf, flags.upgradeTimeout, helm.DenyDestructive)
|
||||
if errors.Is(err, helm.ErrConfirmationMissing) {
|
||||
if !flags.yes {
|
||||
cmd.PrintErrln("WARNING: Upgrading cert-manager will destroy all custom resources you have manually created that are based on the current version of cert-manager.")
|
||||
ok, askErr := askToConfirm(cmd, "Do you want to upgrade cert-manager anyway?")
|
||||
if askErr != nil {
|
||||
return fmt.Errorf("asking for confirmation: %w", err)
|
||||
}
|
||||
if !ok {
|
||||
cmd.Println("Aborting upgrade.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
err = upgrader.UpgradeHelmServices(cmd.Context(), conf, flags.upgradeTimeout, helm.AllowDestructive)
|
||||
}
|
||||
if err := upgrader.UpgradeHelmServices(cmd.Context(), conf, timeout); err != nil {
|
||||
if err != nil {
|
||||
return fmt.Errorf("upgrading helm: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -96,9 +107,39 @@ func upgradeExecute(cmd *cobra.Command, imageFetcher imageFetcher, upgrader clou
|
||||
return upgrader.Upgrade(cmd.Context(), imageReference, conf.Upgrade.Image, conf.Upgrade.Measurements)
|
||||
}
|
||||
|
||||
func parseUpgradeExecuteFlags(cmd *cobra.Command) (upgradeExecuteFlags, error) {
|
||||
configPath, err := cmd.Flags().GetString("config")
|
||||
if err != nil {
|
||||
return upgradeExecuteFlags{}, err
|
||||
}
|
||||
|
||||
helmFlag, err := cmd.Flags().GetBool("helm")
|
||||
if err != nil {
|
||||
return upgradeExecuteFlags{}, err
|
||||
}
|
||||
|
||||
yes, err := cmd.Flags().GetBool("yes")
|
||||
if err != nil {
|
||||
return upgradeExecuteFlags{}, err
|
||||
}
|
||||
|
||||
timeout, err := cmd.Flags().GetDuration("timeout")
|
||||
if err != nil {
|
||||
return upgradeExecuteFlags{}, err
|
||||
}
|
||||
return upgradeExecuteFlags{configPath: configPath, helmFlag: helmFlag, yes: yes, upgradeTimeout: timeout}, nil
|
||||
}
|
||||
|
||||
type upgradeExecuteFlags struct {
|
||||
configPath string
|
||||
helmFlag bool
|
||||
yes bool
|
||||
upgradeTimeout time.Duration
|
||||
}
|
||||
|
||||
type cloudUpgrader interface {
|
||||
Upgrade(ctx context.Context, imageReference, imageVersion string, measurements measurements.M) error
|
||||
UpgradeHelmServices(ctx context.Context, config *config.Config, timeout time.Duration) error
|
||||
UpgradeHelmServices(ctx context.Context, config *config.Config, timeout time.Duration, allowDestructive bool) error
|
||||
}
|
||||
|
||||
type imageFetcher interface {
|
||||
|
@ -75,7 +75,7 @@ func (u stubUpgrader) Upgrade(context.Context, string, string, measurements.M) e
|
||||
return u.err
|
||||
}
|
||||
|
||||
func (u stubUpgrader) UpgradeHelmServices(ctx context.Context, config *config.Config, timeout time.Duration) error {
|
||||
func (u stubUpgrader) UpgradeHelmServices(ctx context.Context, config *config.Config, timeout time.Duration, allowDestructive bool) error {
|
||||
return u.helmErr
|
||||
}
|
||||
|
||||
|
@ -16,21 +16,31 @@ import (
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/edgelesssys/constellation/v2/internal/deploy/helm"
|
||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/mod/semver"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/cli"
|
||||
"helm.sh/helm/v3/pkg/release"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
const (
|
||||
// AllowDestructive is a named bool to signal that destructive actions have been confirmed by the user.
|
||||
AllowDestructive = true
|
||||
// DenyDestructive is a named bool to signal that destructive actions have not been confirmed by the user yet.
|
||||
DenyDestructive = false
|
||||
)
|
||||
|
||||
// Client handles interaction with helm.
|
||||
type Client struct {
|
||||
config *action.Configuration
|
||||
kubectl crdClient
|
||||
fs file.Handler
|
||||
actions actionWrapper
|
||||
log debugLog
|
||||
}
|
||||
|
||||
@ -55,26 +65,26 @@ func NewClient(client crdClient, kubeConfigPath, helmNamespace string, log debug
|
||||
return nil, fmt.Errorf("initializing kubectl: %w", err)
|
||||
}
|
||||
|
||||
return &Client{config: actionConfig, kubectl: client, fs: fileHandler, log: log}, nil
|
||||
return &Client{kubectl: client, fs: fileHandler, actions: actions{config: actionConfig}, log: log}, nil
|
||||
}
|
||||
|
||||
// Upgrade runs a helm-upgrade on all deployments that are managed via Helm.
|
||||
// If the CLI receives an interrupt signal it will cancel the context.
|
||||
// Canceling the context will prompt helm to abort and roll back the ongoing upgrade.
|
||||
func (c *Client) Upgrade(ctx context.Context, config *config.Config, timeout time.Duration) error {
|
||||
if err := c.upgradeRelease(ctx, timeout, ciliumPath, ciliumReleaseName, false); err != nil {
|
||||
func (c *Client) Upgrade(ctx context.Context, config *config.Config, timeout time.Duration, allowDestructive bool) error {
|
||||
if err := c.upgradeRelease(ctx, timeout, ciliumPath, ciliumReleaseName, false, allowDestructive); err != nil {
|
||||
return fmt.Errorf("upgrading cilium: %w", err)
|
||||
}
|
||||
|
||||
if err := c.upgradeRelease(ctx, timeout, certManagerPath, certManagerReleaseName, false); err != nil {
|
||||
if err := c.upgradeRelease(ctx, timeout, certManagerPath, certManagerReleaseName, false, allowDestructive); err != nil {
|
||||
return fmt.Errorf("upgrading cert-manager: %w", err)
|
||||
}
|
||||
|
||||
if err := c.upgradeRelease(ctx, timeout, conOperatorsPath, conOperatorsReleaseName, true); err != nil {
|
||||
if err := c.upgradeRelease(ctx, timeout, conOperatorsPath, conOperatorsReleaseName, true, allowDestructive); err != nil {
|
||||
return fmt.Errorf("upgrading constellation operators: %w", err)
|
||||
}
|
||||
|
||||
if err := c.upgradeRelease(ctx, timeout, conServicesPath, conServicesReleaseName, false); err != nil {
|
||||
if err := c.upgradeRelease(ctx, timeout, conServicesPath, conServicesReleaseName, false, allowDestructive); err != nil {
|
||||
return fmt.Errorf("upgrading constellation-services: %w", err)
|
||||
}
|
||||
|
||||
@ -83,9 +93,7 @@ func (c *Client) Upgrade(ctx context.Context, config *config.Config, timeout tim
|
||||
|
||||
// currentVersion returns the version of the currently installed helm release.
|
||||
func (c *Client) currentVersion(release string) (string, error) {
|
||||
client := action.NewList(c.config)
|
||||
client.Filter = release
|
||||
rel, err := client.Run()
|
||||
rel, err := c.actions.listAction(release)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -104,8 +112,11 @@ func (c *Client) currentVersion(release string) (string, error) {
|
||||
return rel[0].Chart.Metadata.Version, nil
|
||||
}
|
||||
|
||||
// ErrConfirmationMissing signals that an action requires user confirmation.
|
||||
var ErrConfirmationMissing = errors.New("action requires user confirmation")
|
||||
|
||||
func (c *Client) upgradeRelease(
|
||||
ctx context.Context, timeout time.Duration, chartPath, releaseName string, hasCRDs bool,
|
||||
ctx context.Context, timeout time.Duration, chartPath, releaseName string, hasCRDs bool, allowDestructive bool,
|
||||
) error {
|
||||
chart, err := loadChartsDir(helmFS, chartPath)
|
||||
if err != nil {
|
||||
@ -126,6 +137,10 @@ func (c *Client) upgradeRelease(
|
||||
return nil
|
||||
}
|
||||
|
||||
if releaseName == certManagerReleaseName && !allowDestructive {
|
||||
return ErrConfirmationMissing
|
||||
}
|
||||
|
||||
if hasCRDs {
|
||||
if err := c.updateCRDs(ctx, chart); err != nil {
|
||||
return fmt.Errorf("updating CRDs: %w", err)
|
||||
@ -137,13 +152,9 @@ func (c *Client) upgradeRelease(
|
||||
}
|
||||
|
||||
c.log.Debugf("Upgrading %s from %s to %s", releaseName, currentVersion, chart.Metadata.Version)
|
||||
action := action.NewUpgrade(c.config)
|
||||
action.Atomic = true
|
||||
action.Namespace = constants.HelmNamespace
|
||||
action.ReuseValues = false
|
||||
action.Timeout = timeout
|
||||
if _, err := action.RunWithContext(ctx, releaseName, chart, values); err != nil {
|
||||
return fmt.Errorf("upgrading %s: %w", releaseName, err)
|
||||
err = c.actions.upgradeAction(ctx, releaseName, chart, values, timeout)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -159,9 +170,9 @@ func (c *Client) prepareValues(chart *chart.Chart, releaseName string) (map[stri
|
||||
if releaseName == certManagerReleaseName {
|
||||
chart.Values["installCRDs"] = true
|
||||
}
|
||||
values, err := c.GetValues(releaseName)
|
||||
values, err := c.actions.getValues(releaseName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting values: %w", err)
|
||||
return nil, fmt.Errorf("getting values for %s: %w", releaseName, err)
|
||||
}
|
||||
return helm.MergeMaps(chart.Values, values), nil
|
||||
}
|
||||
@ -230,3 +241,40 @@ type crdClient interface {
|
||||
GetCRDs(ctx context.Context) ([]apiextensionsv1.CustomResourceDefinition, error)
|
||||
GetCRs(ctx context.Context, gvr schema.GroupVersionResource) ([]unstructured.Unstructured, error)
|
||||
}
|
||||
|
||||
type actionWrapper interface {
|
||||
listAction(release string) ([]*release.Release, error)
|
||||
getValues(release string) (map[string]any, error)
|
||||
upgradeAction(ctx context.Context, releaseName string, chart *chart.Chart, values map[string]any, timeout time.Duration) error
|
||||
}
|
||||
|
||||
type actions struct {
|
||||
config *action.Configuration
|
||||
}
|
||||
|
||||
// listAction execute a List action by wrapping helm's action package.
|
||||
// It creates the action, runs it at returns results and errors.
|
||||
func (a actions) listAction(release string) ([]*release.Release, error) {
|
||||
action := action.NewList(a.config)
|
||||
action.Filter = release
|
||||
return action.Run()
|
||||
}
|
||||
|
||||
func (a actions) getValues(release string) (map[string]any, error) {
|
||||
client := action.NewGetValues(a.config)
|
||||
// Version corresponds to the releases revision. Specifying a Version <= 0 yields the latest release.
|
||||
client.Version = 0
|
||||
return client.Run(release)
|
||||
}
|
||||
|
||||
func (a actions) upgradeAction(ctx context.Context, releaseName string, chart *chart.Chart, values map[string]any, timeout time.Duration) error {
|
||||
action := action.NewUpgrade(a.config)
|
||||
action.Atomic = true
|
||||
action.Namespace = constants.HelmNamespace
|
||||
action.ReuseValues = false
|
||||
action.Timeout = timeout
|
||||
if _, err := action.RunWithContext(ctx, releaseName, chart, values); err != nil {
|
||||
return fmt.Errorf("upgrading %s: %w", releaseName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -7,9 +7,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
package helm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"helm.sh/helm/v3/pkg/chart"
|
||||
"helm.sh/helm/v3/pkg/release"
|
||||
)
|
||||
|
||||
func TestIsUpgrade(t *testing.T) {
|
||||
@ -77,3 +82,47 @@ func TestIsUpgrade(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgradeRelease(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
allowDestructive bool
|
||||
wantError bool
|
||||
}{
|
||||
"allow": {
|
||||
allowDestructive: true,
|
||||
},
|
||||
"deny": {
|
||||
allowDestructive: false,
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
client := Client{kubectl: nil, actions: &stubActionWrapper{}, log: logger.NewTest(t)}
|
||||
err := client.upgradeRelease(context.Background(), 0, certManagerPath, certManagerReleaseName, false, tc.allowDestructive)
|
||||
if tc.wantError {
|
||||
assert.ErrorIs(err, ErrConfirmationMissing)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type stubActionWrapper struct{}
|
||||
|
||||
// listAction returns a list of len 1 with a release that has only it's version set.
|
||||
func (a *stubActionWrapper) listAction(_ string) ([]*release.Release, error) {
|
||||
return []*release.Release{{Chart: &chart.Chart{Metadata: &chart.Metadata{Version: "1.0.0"}}}}, nil
|
||||
}
|
||||
|
||||
func (a *stubActionWrapper) getValues(release string) (map[string]any, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (a *stubActionWrapper) upgradeAction(ctx context.Context, releaseName string, chart *chart.Chart, values map[string]any, timeout time.Duration) error {
|
||||
return nil
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user