constellation/internal/versionsapi/cli/add.go
Otto Bittner c275464634 cli: change upgrade-plan to upgrade-check
Upgrade check is used to find updates for the current cluster.
Optionally the found upgrades can be persisted to the config
for consumption by the upgrade-execute cmd.
The old `upgrade execute` in this commit does not work with
the new `upgrade plan`.
The current versions are read from the cluster.
Supported versions are read from the cli and the versionsapi.
Adds a new config field MicroserviceVersion that will be used
by `upgrade execute` to update the service versions.
The field is optional until 2.7
A deprecation warning for the upgrade key is printed during
config validation.
Kubernetes versions now specify the patch version to make it
explicit for users if an upgrade changes the k8s version.
2023-02-08 12:30:01 +01:00

283 lines
7.8 KiB
Go

/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package main
import (
"context"
"errors"
"fmt"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
verclient "github.com/edgelesssys/constellation/v2/internal/versionsapi/client"
"github.com/spf13/cobra"
"go.uber.org/zap/zapcore"
"golang.org/x/mod/semver"
)
func newAddCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "add",
Short: "Add a new version",
Long: `Add a new version to the versions API.
Developers should not use this command directly. It is invoked by the CI/CD pipeline.
If you've build a local image, use an local override instead of adding a new version.
❗ If you use the command nevertheless, you better know what you do.
`,
RunE: runAdd,
}
cmd.Flags().String("ref", "", "Ref of the version to add")
cmd.Flags().String("stream", "", "Stream of the version to add")
cmd.Flags().String("version", "", "Version to add (format: \"v1.2.3\")")
cmd.Flags().Bool("latest", false, "Whether the version is the latest version of the ref/stream")
cmd.Flags().Bool("release", false, "Whether the version is a release version")
cmd.Flags().Bool("dryrun", false, "Whether to run in dry-run mode (no changes are made)")
cmd.MarkFlagsMutuallyExclusive("ref", "release")
must(cmd.MarkFlagRequired("version"))
return cmd
}
func runAdd(cmd *cobra.Command, args []string) (retErr error) {
flags, err := parseAddFlags(cmd)
if err != nil {
return err
}
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
log.Debugf("Validating flags")
if err := flags.validate(log); err != nil {
return err
}
log.Debugf("Creating version struct")
ver := versionsapi.Version{
Ref: flags.ref,
Stream: flags.stream,
Version: flags.version,
Kind: versionsapi.VersionKindImage,
}
if err := ver.Validate(); err != nil {
return err
}
log.Debugf("Creating versions API client")
client, err := verclient.NewClient(cmd.Context(), flags.region, flags.bucket, flags.distributionID, flags.dryRun, log)
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
defer func(retErr *error) {
log.Infof("Invalidating cache. This may take some time")
if err := client.InvalidateCache(cmd.Context()); err != nil && retErr == nil {
*retErr = fmt.Errorf("invalidating cache: %w", err)
}
}(&retErr)
log.Infof("Adding version")
if err := ensureVersion(cmd.Context(), client, ver, versionsapi.GranularityMajor, log); err != nil {
return err
}
if err := ensureVersion(cmd.Context(), client, ver, versionsapi.GranularityMinor, log); err != nil {
return err
}
if flags.latest {
if err := updateLatest(cmd.Context(), client, ver, log); err != nil {
return fmt.Errorf("setting latest version: %w", err)
}
}
log.Infof("List major->minor URL: %s", ver.ListURL(versionsapi.GranularityMajor))
log.Infof("List minor->patch URL: %s", ver.ListURL(versionsapi.GranularityMinor))
return nil
}
func ensureVersion(ctx context.Context, client *verclient.Client, ver versionsapi.Version, gran versionsapi.Granularity,
log *logger.Logger,
) error {
verListReq := versionsapi.List{
Ref: ver.Ref,
Stream: ver.Stream,
Granularity: gran,
Base: ver.WithGranularity(gran),
Kind: versionsapi.VersionKindImage,
}
verList, err := client.FetchVersionList(ctx, verListReq)
var notFoundErr *verclient.NotFoundError
if errors.As(err, &notFoundErr) {
log.Infof("Version list for %s versions under %q does not exist. Creating new list", gran.String(), ver.Major())
verList = verListReq
} else if err != nil {
return fmt.Errorf("failed to list minor versions: %w", err)
}
log.Debugf("%s version list: %v", gran.String(), verList)
insertGran := gran + 1
insertVersion := ver.WithGranularity(insertGran)
if verList.Contains(insertVersion) {
log.Infof("Version %q already exists in list %v", insertVersion, verList.Versions)
return nil
}
log.Infof("Inserting %s version %q into list", insertGran.String(), insertVersion)
verList.Versions = append(verList.Versions, insertVersion)
log.Debugf("New %s version list: %v", gran.String(), verList)
if err := client.UpdateVersionList(ctx, verList); err != nil {
return fmt.Errorf("failed to add %s version: %w", gran.String(), err)
}
log.Infof("Added %q to list", insertVersion)
return nil
}
func updateLatest(ctx context.Context, client *verclient.Client, ver versionsapi.Version, log *logger.Logger) error {
latest := versionsapi.Latest{
Ref: ver.Ref,
Stream: ver.Stream,
Kind: versionsapi.VersionKindImage,
}
latest, err := client.FetchVersionLatest(ctx, latest)
var notFoundErr *verclient.NotFoundError
if errors.As(err, &notFoundErr) {
log.Debugf("Latest version for ref %q and stream %q not found", ver.Ref, ver.Stream)
} else if err != nil {
return fmt.Errorf("fetching latest version: %w", err)
}
if latest.Version == ver.Version {
log.Infof("Version %q is already latest version", ver)
return nil
}
log.Infof("Setting %q as latest version", ver)
latest = versionsapi.Latest{
Ref: ver.Ref,
Stream: ver.Stream,
Version: ver.Version,
Kind: versionsapi.VersionKindImage,
}
if err := client.UpdateVersionLatest(ctx, latest); err != nil {
return fmt.Errorf("updating latest version: %w", err)
}
return nil
}
type addFlags struct {
version string
stream string
ref string
release bool
latest bool
dryRun bool
region string
bucket string
distributionID string
logLevel zapcore.Level
}
func (f *addFlags) validate(log *logger.Logger) error {
if !semver.IsValid(f.version) {
return fmt.Errorf("version %q is not a valid semantic version", f.version)
}
if semver.Canonical(f.version) != f.version {
return fmt.Errorf("version %q is not a canonical semantic version", f.version)
}
if f.ref == "" && !f.release {
return fmt.Errorf("either --ref or --release must be set")
}
if f.release {
log.Debugf("Setting ref to %q, as release flag is set", versionsapi.ReleaseRef)
f.ref = versionsapi.ReleaseRef
} else {
log.Debugf("Setting latest to true, as release flag is not set")
f.latest = true // always set latest for non-release versions
}
if err := versionsapi.ValidateRef(f.ref); err != nil {
return fmt.Errorf("invalid ref %w", err)
}
if err := versionsapi.ValidateStream(f.ref, f.stream); err != nil {
return fmt.Errorf("invalid stream %w", err)
}
return nil
}
func parseAddFlags(cmd *cobra.Command) (addFlags, error) {
ref, err := cmd.Flags().GetString("ref")
if err != nil {
return addFlags{}, err
}
ref = versionsapi.CanonicalizeRef(ref)
stream, err := cmd.Flags().GetString("stream")
if err != nil {
return addFlags{}, err
}
version, err := cmd.Flags().GetString("version")
if err != nil {
return addFlags{}, err
}
release, err := cmd.Flags().GetBool("release")
if err != nil {
return addFlags{}, err
}
latest, err := cmd.Flags().GetBool("latest")
if err != nil {
return addFlags{}, err
}
dryRun, err := cmd.Flags().GetBool("dryrun")
if err != nil {
return addFlags{}, err
}
verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return addFlags{}, err
}
logLevel := zapcore.InfoLevel
if verbose {
logLevel = zapcore.DebugLevel
}
region, err := cmd.Flags().GetString("region")
if err != nil {
return addFlags{}, err
}
bucket, err := cmd.Flags().GetString("bucket")
if err != nil {
return addFlags{}, err
}
distributionID, err := cmd.Flags().GetString("distribution-id")
if err != nil {
return addFlags{}, err
}
return addFlags{
version: version,
stream: stream,
ref: versionsapi.CanonicalizeRef(ref),
release: release,
latest: latest,
dryRun: dryRun,
region: region,
bucket: bucket,
distributionID: distributionID,
logLevel: logLevel,
}, nil
}