mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-28 00:49:26 -05:00
291 lines
8.4 KiB
Go
291 lines
8.4 KiB
Go
/*
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
apiclient "github.com/edgelesssys/constellation/v2/internal/api/client"
|
|
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
|
"github.com/edgelesssys/constellation/v2/internal/logger"
|
|
"github.com/spf13/cobra"
|
|
"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 a 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().String("kind", "", "Version kind to add (e.g. image, cli)")
|
|
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, _ []string) (retErr error) {
|
|
flags, err := parseAddFlags(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
log := logger.NewTextLogger(flags.logLevel)
|
|
log.Debug("Using flags", "dryRun", flags.dryRun, "kind", flags.kind, "latest", flags.latest, "ref", flags.ref,
|
|
"release", flags.release, "stream", flags.stream, "version", flags.version)
|
|
|
|
log.Debug("Validating flags")
|
|
if err := flags.validate(log); err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debug("Creating version struct")
|
|
ver, err := versionsapi.NewVersion(flags.ref, flags.stream, flags.version, flags.kind)
|
|
if err != nil {
|
|
return fmt.Errorf("creating version: %w", err)
|
|
}
|
|
|
|
log.Debug("Creating versions API client")
|
|
client, clientClose, err := versionsapi.NewClient(cmd.Context(), flags.region, flags.bucket, flags.distributionID, flags.dryRun, log)
|
|
if err != nil {
|
|
return fmt.Errorf("creating client: %w", err)
|
|
}
|
|
defer func() {
|
|
err := clientClose(cmd.Context())
|
|
if err != nil {
|
|
retErr = errors.Join(retErr, fmt.Errorf("failed to invalidate cache: %w", err))
|
|
}
|
|
}()
|
|
|
|
log.Info("Adding version")
|
|
if err := ensureVersion(cmd.Context(), client, flags.kind, ver, versionsapi.GranularityMajor, log); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ensureVersion(cmd.Context(), client, flags.kind, ver, versionsapi.GranularityMinor, log); err != nil {
|
|
return err
|
|
}
|
|
|
|
if flags.latest {
|
|
if err := updateLatest(cmd.Context(), client, flags.kind, ver, log); err != nil {
|
|
return fmt.Errorf("setting latest version: %w", err)
|
|
}
|
|
}
|
|
|
|
log.Info(fmt.Sprintf("List major->minor URL: %s", ver.ListURL(versionsapi.GranularityMajor)))
|
|
log.Info(fmt.Sprintf("List minor->patch URL: %s", ver.ListURL(versionsapi.GranularityMinor)))
|
|
|
|
return nil
|
|
}
|
|
|
|
func ensureVersion(ctx context.Context, client *versionsapi.Client, kind versionsapi.VersionKind, ver versionsapi.Version, gran versionsapi.Granularity,
|
|
log *slog.Logger,
|
|
) error {
|
|
verListReq := versionsapi.List{
|
|
Ref: ver.Ref(),
|
|
Stream: ver.Stream(),
|
|
Granularity: gran,
|
|
Base: ver.WithGranularity(gran),
|
|
Kind: kind,
|
|
}
|
|
verList, err := client.FetchVersionList(ctx, verListReq)
|
|
var notFoundErr *apiclient.NotFoundError
|
|
if errors.As(err, ¬FoundErr) {
|
|
log.Info(fmt.Sprintf("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.Debug(fmt.Sprintf("%q version list: %v", gran.String(), verList.Versions))
|
|
|
|
insertGran := gran + 1
|
|
insertVersion := ver.WithGranularity(insertGran)
|
|
|
|
if verList.Contains(insertVersion) {
|
|
log.Info(fmt.Sprintf("Version %q already exists in list %v", insertVersion, verList.Versions))
|
|
return nil
|
|
}
|
|
log.Info(fmt.Sprintf("Inserting %s version %q into list", insertGran.String(), insertVersion))
|
|
|
|
verList.Versions = append(verList.Versions, insertVersion)
|
|
log.Debug(fmt.Sprintf("New %q version list: %v", gran.String(), verList.Versions))
|
|
|
|
if err := client.UpdateVersionList(ctx, verList); err != nil {
|
|
return fmt.Errorf("failed to add %s version: %w", gran.String(), err)
|
|
}
|
|
|
|
log.Info(fmt.Sprintf("Added %q to list", insertVersion))
|
|
return nil
|
|
}
|
|
|
|
func updateLatest(ctx context.Context, client *versionsapi.Client, kind versionsapi.VersionKind, ver versionsapi.Version, log *slog.Logger) error {
|
|
latest := versionsapi.Latest{
|
|
Ref: ver.Ref(),
|
|
Stream: ver.Stream(),
|
|
Kind: kind,
|
|
}
|
|
latest, err := client.FetchVersionLatest(ctx, latest)
|
|
var notFoundErr *apiclient.NotFoundError
|
|
if errors.As(err, ¬FoundErr) {
|
|
log.Debug(fmt.Sprintf("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.Info(fmt.Sprintf("Version %q is already latest version", ver.Version()))
|
|
return nil
|
|
}
|
|
|
|
log.Info(fmt.Sprintf("Setting %q as latest version", ver.Version()))
|
|
latest = versionsapi.Latest{
|
|
Ref: ver.Ref(),
|
|
Stream: ver.Stream(),
|
|
Version: ver.Version(),
|
|
Kind: kind,
|
|
}
|
|
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
|
|
kind versionsapi.VersionKind
|
|
logLevel slog.Level
|
|
}
|
|
|
|
func (f *addFlags) validate(log *slog.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.kind == versionsapi.VersionKindUnknown {
|
|
return fmt.Errorf("unknown version kind %q", f.kind)
|
|
}
|
|
|
|
if f.release {
|
|
log.Debug(fmt.Sprintf("Setting ref to %q, as release flag is set", versionsapi.ReleaseRef))
|
|
f.ref = versionsapi.ReleaseRef
|
|
} else {
|
|
log.Debug("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
|
|
}
|
|
kindFlag, err := cmd.Flags().GetString("kind")
|
|
if err != nil {
|
|
return addFlags{}, err
|
|
}
|
|
kind := versionsapi.VersionKindFromString(kindFlag)
|
|
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 := slog.LevelInfo
|
|
if verbose {
|
|
logLevel = slog.LevelDebug
|
|
}
|
|
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,
|
|
kind: kind,
|
|
}, nil
|
|
}
|