Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cli: add constellation apply command to replace init and upgrade apply #2484

Merged
merged 8 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/actions/constellation_create/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,12 @@ runs:
id: constellation-init
shell: bash
run: |
constellation init --debug
# TODO(v2.14): Remove workaround for CLIs not supporting apply command
cmd=apply
if constellation --help | grep -q init; then
cmd=init
fi
constellation $cmd --debug
echo "KUBECONFIG=$(pwd)/constellation-admin.conf" | tee -a $GITHUB_OUTPUT

- name: Wait for nodes to join and become ready
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ jobs:
- name: Initialize cluster
shell: pwsh
run: |
.\constellation.exe init --debug
.\constellation.exe apply --debug

- name: Liveness probe
shell: pwsh
Expand Down
3 changes: 2 additions & 1 deletion cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func NewRootCmd() *cobra.Command {

rootCmd.AddCommand(cmd.NewConfigCmd())
rootCmd.AddCommand(cmd.NewCreateCmd())
rootCmd.AddCommand(cmd.NewInitCmd())
rootCmd.AddCommand(cmd.NewApplyCmd())
rootCmd.AddCommand(cmd.NewMiniCmd())
rootCmd.AddCommand(cmd.NewStatusCmd())
rootCmd.AddCommand(cmd.NewVerifyCmd())
Expand All @@ -60,6 +60,7 @@ func NewRootCmd() *cobra.Command {
rootCmd.AddCommand(cmd.NewTerminateCmd())
rootCmd.AddCommand(cmd.NewIAMCmd())
rootCmd.AddCommand(cmd.NewVersionCmd())
rootCmd.AddCommand(cmd.NewInitCmd())

return rootCmd
}
Expand Down
115 changes: 99 additions & 16 deletions cli/internal/cmd/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,85 @@ import (
k8serrors "k8s.io/apimachinery/pkg/api/errors"
)

// phases that can be skipped during apply.
// New phases should also be added to [formatSkipPhases].
const (
// skipInfrastructurePhase skips the Terraform apply of the apply process.
skipInfrastructurePhase skipPhase = "infrastructure"
// skipInitPhase skips the init RPC of the apply process.
skipInitPhase skipPhase = "init"
// skipAttestationConfigPhase skips the attestation config upgrade of the apply process.
skipAttestationConfigPhase skipPhase = "attestationconfig"
// skipCertSANsPhase skips the cert SANs upgrade of the apply process.
skipCertSANsPhase skipPhase = "certsans"
// skipHelmPhase skips the helm upgrade of the apply process.
skipHelmPhase skipPhase = "helm"
// skipImagePhase skips the image upgrade of the apply process.
skipImagePhase skipPhase = "image"
// skipK8sPhase skips the Kubernetes version upgrade of the apply process.
skipK8sPhase skipPhase = "k8s"
)

// formatSkipPhases returns a formatted string of all phases that can be skipped.
func formatSkipPhases() string {
return fmt.Sprintf("{ %s }", strings.Join([]string{
string(skipInfrastructurePhase),
string(skipInitPhase),
string(skipAttestationConfigPhase),
string(skipCertSANsPhase),
string(skipHelmPhase),
string(skipImagePhase),
string(skipK8sPhase),
}, " | "))
}

// skipPhase is a phase of the upgrade process that can be skipped.
type skipPhase string

// skipPhases is a list of phases that can be skipped during the upgrade process.
type skipPhases map[skipPhase]struct{}

// contains returns true if the list of phases contains the given phase.
func (s skipPhases) contains(phase skipPhase) bool {
_, ok := s[skipPhase(strings.ToLower(string(phase)))]
return ok
}

// add a phase to the list of phases.
func (s *skipPhases) add(phases ...skipPhase) {
if *s == nil {
*s = make(skipPhases)
}
for _, phase := range phases {
(*s)[skipPhase(strings.ToLower(string(phase)))] = struct{}{}
}
}

// NewApplyCmd creates the apply command.
func NewApplyCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "apply",
Short: "Apply a configuration to a Constellation cluster",
Long: "Apply a configuration to a Constellation cluster to initialize or upgrade the cluster.",
Args: cobra.NoArgs,
RunE: runApply,
}

cmd.Flags().Bool("conformance", false, "enable conformance mode")
cmd.Flags().Bool("skip-helm-wait", false, "install helm charts without waiting for deployments to be ready")
cmd.Flags().Bool("merge-kubeconfig", false, "merge Constellation kubeconfig file with default kubeconfig file in $HOME/.kube/config")
elchead marked this conversation as resolved.
Show resolved Hide resolved
cmd.Flags().BoolP("yes", "y", false, "run command without further confirmation\n"+
"WARNING: the command might delete or update existing resources without additional checks. Please read the docs.\n")
cmd.Flags().Duration("timeout", 5*time.Minute, "change helm upgrade timeout\n"+
"Might be useful for slow connections or big clusters.")
cmd.Flags().StringSlice("skip-phases", nil, "comma-separated list of upgrade phases to skip\n"+
fmt.Sprintf("one or multiple of %s", formatSkipPhases()))

must(cmd.Flags().MarkHidden("timeout"))

return cmd
}

// applyFlags defines the flags for the apply command.
type applyFlags struct {
rootFlags
Expand Down Expand Up @@ -202,7 +281,7 @@ The control flow is as follows:
│ │Not up to date │
│ │(Diff from Terraform plan)│
│ └────────────┐ │
│ │ │Terraform
│ │ |Infrastructure
│ ┌────────────▼──────────┐ │Phase
│ │Apply Terraform updates│ │
│ └────────────┬──────────┘ │
Expand All @@ -228,14 +307,14 @@ The control flow is as follows:
└──────────────┬───────────────┘ │ │
│ │ │
└───────────────┐ │ ───┘
│ │
┌──────────▼──▼──────────┐
│Apply Attestation Config│
└─────────────┬──────────┘
┌──────────────▼────────────┐
│Extend API Server Cert SANs│
└──────────────┬────────────┘
│ │ ───┐
┌──────────▼──▼──────────┐ │AttestationConfig
│Apply Attestation Config│ │Phase
└─────────────┬──────────┘ ───┘
───┐
┌──────────────▼────────────┐ │CertSANs
│Extend API Server Cert SANs│ │Phase
└──────────────┬────────────┘ ───┘
│ ───┐
┌──────────▼────────┐ │Helm
│ Apply Helm Charts │ │Phase
Expand Down Expand Up @@ -277,22 +356,26 @@ func (a *applyCmd) apply(cmd *cobra.Command, configFetcher attestationconfigapi.
}

// From now on we can assume a valid Kubernetes admin config file exists
a.log.Debugf("Creating Kubernetes client using %s", a.flags.pathPrefixer.PrefixPrintablePath(constants.AdminConfFilename))
kubeUpgrader, err := a.newKubeUpgrader(cmd.OutOrStdout(), constants.AdminConfFilename, a.log)
if err != nil {
return err
}

// Apply Attestation Config
a.log.Debugf("Creating Kubernetes client using %s", a.flags.pathPrefixer.PrefixPrintablePath(constants.AdminConfFilename))
a.log.Debugf("Applying new attestation config to cluster")
if err := a.applyJoinConfig(cmd, kubeUpgrader, conf.GetAttestationConfig(), stateFile.ClusterValues.MeasurementSalt); err != nil {
return fmt.Errorf("applying attestation config: %w", err)
if !a.flags.skipPhases.contains(skipAttestationConfigPhase) {
a.log.Debugf("Applying new attestation config to cluster")
if err := a.applyJoinConfig(cmd, kubeUpgrader, conf.GetAttestationConfig(), stateFile.ClusterValues.MeasurementSalt); err != nil {
return fmt.Errorf("applying attestation config: %w", err)
}
}

// Extend API Server Cert SANs
sans := append([]string{stateFile.Infrastructure.ClusterEndpoint, conf.CustomEndpoint}, stateFile.Infrastructure.APIServerCertSANs...)
if err := kubeUpgrader.ExtendClusterConfigCertSANs(cmd.Context(), sans); err != nil {
return fmt.Errorf("extending cert SANs: %w", err)
if !a.flags.skipPhases.contains(skipCertSANsPhase) {
sans := append([]string{stateFile.Infrastructure.ClusterEndpoint, conf.CustomEndpoint}, stateFile.Infrastructure.APIServerCertSANs...)
if err := kubeUpgrader.ExtendClusterConfigCertSANs(cmd.Context(), sans); err != nil {
return fmt.Errorf("extending cert SANs: %w", err)
}
}

// Apply Helm Charts
Expand Down
22 changes: 10 additions & 12 deletions cli/internal/cmd/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"context"
"fmt"
"testing"
"time"

"github.com/edgelesssys/constellation/v2/cli/internal/helm"
"github.com/edgelesssys/constellation/v2/internal/file"
Expand All @@ -22,19 +23,13 @@ import (

func TestParseApplyFlags(t *testing.T) {
require := require.New(t)
// TODO: Use flags := applyCmd().Flags() once we have a separate apply command
defaultFlags := func() *pflag.FlagSet {
flags := pflag.NewFlagSet("test", pflag.ContinueOnError)
flags := NewApplyCmd().Flags()
// Register persistent flags
flags.String("workspace", "", "")
flags.String("tf-log", "NONE", "")
flags.Bool("force", false, "")
flags.Bool("debug", false, "")
flags.Bool("merge-kubeconfig", false, "")
flags.Bool("conformance", false, "")
flags.Bool("skip-helm-wait", false, "")
flags.Bool("yes", false, "")
flags.StringSlice("skip-phases", []string{}, "")
flags.Duration("timeout", 0, "")
return flags
}

Expand All @@ -46,7 +41,8 @@ func TestParseApplyFlags(t *testing.T) {
"default flags": {
flags: defaultFlags(),
wantFlags: applyFlags{
helmWaitMode: helm.WaitModeAtomic,
helmWaitMode: helm.WaitModeAtomic,
upgradeTimeout: 5 * time.Minute,
},
},
"skip phases": {
Expand All @@ -56,8 +52,9 @@ func TestParseApplyFlags(t *testing.T) {
return flags
}(),
wantFlags: applyFlags{
skipPhases: skipPhases{skipHelmPhase: struct{}{}, skipK8sPhase: struct{}{}},
helmWaitMode: helm.WaitModeAtomic,
skipPhases: skipPhases{skipHelmPhase: struct{}{}, skipK8sPhase: struct{}{}},
helmWaitMode: helm.WaitModeAtomic,
upgradeTimeout: 5 * time.Minute,
},
},
"skip helm wait": {
Expand All @@ -67,7 +64,8 @@ func TestParseApplyFlags(t *testing.T) {
return flags
}(),
wantFlags: applyFlags{
helmWaitMode: helm.WaitModeNone,
helmWaitMode: helm.WaitModeNone,
upgradeTimeout: 5 * time.Minute,
},
},
}
Expand Down
1 change: 1 addition & 0 deletions cli/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func NewInitCmd() *cobra.Command {
cmd.Flags().Duration("timeout", time.Hour, "")
return runApply(cmd, args)
},
Deprecated: "use 'constellation apply' instead.",
}
cmd.Flags().Bool("conformance", false, "enable conformance mode")
cmd.Flags().Bool("skip-helm-wait", false, "install helm charts without waiting for deployments to be ready")
Expand Down
37 changes: 1 addition & 36 deletions cli/internal/cmd/upgradeapply.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"context"
"fmt"
"io"
"strings"
"time"

"github.com/edgelesssys/constellation/v2/cli/internal/state"
Expand All @@ -24,22 +23,6 @@ import (
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
)

const (
// skipInitPhase skips the init RPC of the apply process.
skipInitPhase skipPhase = "init"
// skipInfrastructurePhase skips the terraform apply of the upgrade process.
skipInfrastructurePhase skipPhase = "infrastructure"
// skipHelmPhase skips the helm upgrade of the upgrade process.
skipHelmPhase skipPhase = "helm"
// skipImagePhase skips the image upgrade of the upgrade process.
skipImagePhase skipPhase = "image"
// skipK8sPhase skips the k8s upgrade of the upgrade process.
skipK8sPhase skipPhase = "k8s"
)

// skipPhase is a phase of the upgrade process that can be skipped.
type skipPhase string

func newUpgradeApplyCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "apply",
Expand All @@ -51,6 +34,7 @@ func newUpgradeApplyCmd() *cobra.Command {
cmd.Flags().Bool("merge-kubeconfig", false, "")
return runApply(cmd, args)
},
Deprecated: "use 'constellation apply' instead.",
}

cmd.Flags().BoolP("yes", "y", false, "run upgrades without further confirmation\n"+
Expand Down Expand Up @@ -81,25 +65,6 @@ func diffAttestationCfg(currentAttestationCfg config.AttestationCfg, newAttestat
return diff, nil
}

// skipPhases is a list of phases that can be skipped during the upgrade process.
type skipPhases map[skipPhase]struct{}

// contains returns true if the list of phases contains the given phase.
func (s skipPhases) contains(phase skipPhase) bool {
_, ok := s[skipPhase(strings.ToLower(string(phase)))]
return ok
}

// add a phase to the list of phases.
func (s *skipPhases) add(phases ...skipPhase) {
if *s == nil {
*s = make(skipPhases)
}
for _, phase := range phases {
(*s)[skipPhase(strings.ToLower(string(phase)))] = struct{}{}
}
}

type kubernetesUpgrader interface {
UpgradeNodeVersion(ctx context.Context, conf *config.Config, force, skipImage, skipK8s bool) error
ExtendClusterConfigCertSANs(ctx context.Context, alternativeNames []string) error
Expand Down
Loading