mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-01-27 15:57:04 -05:00
versionsapi: create cli, implement add cmd
Signed-off-by: Paul Meyer <49727155+katexochen@users.noreply.github.com>
This commit is contained in:
parent
b3f251909c
commit
5679c9d3de
282
internal/versionsapi/cli/add.go
Normal file
282
internal/versionsapi/cli/add.go
Normal file
@ -0,0 +1,282 @@
|
||||
/*
|
||||
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, ¬FoundErr) {
|
||||
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, ¬FoundErr) {
|
||||
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
|
||||
}
|
86
internal/versionsapi/cli/main.go
Normal file
86
internal/versionsapi/cli/main.go
Normal file
@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func execute() error {
|
||||
rootCmd := newRootCmd()
|
||||
ctx, cancel := signalContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
return rootCmd.ExecuteContext(ctx)
|
||||
}
|
||||
|
||||
func newRootCmd() *cobra.Command {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "versionsapi",
|
||||
Short: "Interact with the Constellation versions API",
|
||||
Long: "Interact with the Constellation versions API.",
|
||||
PersistentPreRun: preRunRoot,
|
||||
}
|
||||
|
||||
rootCmd.SetOut(os.Stdout)
|
||||
|
||||
rootCmd.PersistentFlags().Bool("verbose", false, "Enable verbose output")
|
||||
rootCmd.PersistentFlags().String("region", "eu-central-1", "AWS region of the API S3 bucket")
|
||||
rootCmd.PersistentFlags().String("bucket", "cdn-constellation-backend", "S3 bucket name of the API")
|
||||
rootCmd.PersistentFlags().String("distribution-id", "E1H77EZTHC3NE4", "CloudFront distribution ID of the API")
|
||||
|
||||
rootCmd.AddCommand(newAddCmd())
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
// signalContext returns a context that is canceled on the handed signal.
|
||||
// The signal isn't watched after its first occurrence. Call the cancel
|
||||
// function to ensure the internal goroutine is stopped and the signal isn't
|
||||
// watched any longer.
|
||||
func signalContext(ctx context.Context, sig os.Signal) (context.Context, context.CancelFunc) {
|
||||
sigCtx, stop := signal.NotifyContext(ctx, sig)
|
||||
done := make(chan struct{}, 1)
|
||||
stopDone := make(chan struct{}, 1)
|
||||
|
||||
go func() {
|
||||
defer func() { stopDone <- struct{}{} }()
|
||||
defer stop()
|
||||
select {
|
||||
case <-sigCtx.Done():
|
||||
fmt.Println(" Signal caught. Press ctrl+c again to terminate the program immediately.")
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
|
||||
cancelFunc := func() {
|
||||
done <- struct{}{}
|
||||
<-stopDone
|
||||
}
|
||||
|
||||
return sigCtx, cancelFunc
|
||||
}
|
||||
|
||||
func preRunRoot(cmd *cobra.Command, args []string) {
|
||||
cmd.SilenceUsage = true
|
||||
}
|
||||
|
||||
func must(err error) {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user