/* Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ package main import ( "context" "encoding/json" "errors" "fmt" "log/slog" "github.com/spf13/cobra" "golang.org/x/mod/semver" apiclient "github.com/edgelesssys/constellation/v2/internal/api/client" "github.com/edgelesssys/constellation/v2/internal/api/versionsapi" "github.com/edgelesssys/constellation/v2/internal/logger" ) func newListCmd() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "List versions", Long: "List all versions of a ref/stream. The returned version are in short format, if --json flag is not set.", RunE: runList, Args: cobra.ExactArgs(0), } cmd.Flags().String("ref", "-", "Ref to query") cmd.Flags().String("stream", "stable", "Stream to query") cmd.Flags().String("minor-version", "", "Minor version to query (format: \"v1.2\")") cmd.Flags().Bool("json", false, "Whether to output the result as JSON") return cmd } func runList(cmd *cobra.Command, _ []string) (retErr error) { flags, err := parseListFlags(cmd) if err != nil { return err } log := logger.NewTextLogger(flags.logLevel) log.Debug(fmt.Sprintf("Parsed flags: %+v", flags)) log.Debug("Validating flags") if err := flags.validate(); err != nil { return err } log.Debug("Creating versions API client") client, clientClose, err := versionsapi.NewReadOnlyClient(cmd.Context(), flags.region, flags.bucket, flags.distributionID, 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)) } }() var minorVersions []string if flags.minorVersion != "" { minorVersions = []string{flags.minorVersion} } else { log.Debug("Getting minor versions") minorVersions, err = listMinorVersions(cmd.Context(), client, flags.ref, flags.stream) var errNotFound *apiclient.NotFoundError if err != nil && errors.As(err, &errNotFound) { log.Info(fmt.Sprintf("No minor versions found for ref %q and stream %q.", flags.ref, flags.stream)) return nil } else if err != nil { return err } } log.Debug("Getting patch versions") patchVersions, err := listPatchVersions(cmd.Context(), client, flags.ref, flags.stream, minorVersions) var errNotFound *apiclient.NotFoundError if err != nil && errors.As(err, &errNotFound) { log.Info(fmt.Sprintf("No patch versions found for ref %q, stream %q and minor versions %v.", flags.ref, flags.stream, minorVersions)) return nil } else if err != nil { return err } if flags.json { log.Debug("Printing versions as JSON") var vers []string for _, v := range patchVersions { vers = append(vers, v.Version()) } raw, err := json.MarshalIndent(vers, "", " ") if err != nil { return fmt.Errorf("marshaling versions: %w", err) } fmt.Println(string(raw)) return nil } log.Debug("Printing versions") for _, v := range patchVersions { fmt.Println(v.ShortPath()) } return nil } func listMinorVersions(ctx context.Context, client *versionsapi.Client, ref string, stream string) ([]string, error) { list := versionsapi.List{ Ref: ref, Stream: stream, Granularity: versionsapi.GranularityMajor, Base: "v2", Kind: versionsapi.VersionKindImage, } list, err := client.FetchVersionList(ctx, list) if err != nil { return nil, fmt.Errorf("listing minor versions: %w", err) } return list.Versions, nil } func listPatchVersions(ctx context.Context, client *versionsapi.Client, ref string, stream string, minorVer []string, ) ([]versionsapi.Version, error) { var patchVers []versionsapi.Version list := versionsapi.List{ Ref: ref, Stream: stream, Granularity: versionsapi.GranularityMinor, Kind: versionsapi.VersionKindImage, } for _, ver := range minorVer { list.Base = ver list, err := client.FetchVersionList(ctx, list) if err != nil { return nil, fmt.Errorf("listing patch versions: %w", err) } patchVers = append(patchVers, list.StructuredVersions()...) } return patchVers, nil } type listFlags struct { ref string stream string minorVersion string region string bucket string distributionID string json bool logLevel slog.Level } func (l *listFlags) validate() error { if err := versionsapi.ValidateRef(l.ref); err != nil { return fmt.Errorf("invalid ref: %w", err) } if err := versionsapi.ValidateStream(l.ref, l.stream); err != nil { return fmt.Errorf("invalid stream: %w", err) } if l.minorVersion != "" { if !semver.IsValid(l.minorVersion) || semver.MajorMinor(l.minorVersion) != l.minorVersion { return fmt.Errorf("invalid minor version: %q", l.minorVersion) } } return nil } func parseListFlags(cmd *cobra.Command) (listFlags, error) { ref, err := cmd.Flags().GetString("ref") if err != nil { return listFlags{}, err } ref = versionsapi.CanonicalizeRef(ref) stream, err := cmd.Flags().GetString("stream") if err != nil { return listFlags{}, err } minorVersion, err := cmd.Flags().GetString("minor-version") if err != nil { return listFlags{}, err } region, err := cmd.Flags().GetString("region") if err != nil { return listFlags{}, err } bucket, err := cmd.Flags().GetString("bucket") if err != nil { return listFlags{}, err } distributionID, err := cmd.Flags().GetString("distribution-id") if err != nil { return listFlags{}, err } json, err := cmd.Flags().GetBool("json") if err != nil { return listFlags{}, err } verbose, err := cmd.Flags().GetBool("verbose") if err != nil { return listFlags{}, err } logLevel := slog.LevelInfo if verbose { logLevel = slog.LevelDebug } return listFlags{ ref: ref, stream: stream, minorVersion: minorVersion, region: region, bucket: bucket, distributionID: distributionID, json: json, logLevel: logLevel, }, nil }