mirror of
				https://github.com/edgelesssys/constellation.git
				synced 2025-10-31 11:49:02 -04:00 
			
		
		
		
	 85d723ccbd
			
		
	
	
		85d723ccbd
		
	
	
	
	
		
			
			- Rename "updates" -> "versions" - Add explicit "stream" in path to make API self-describing
		
			
				
	
	
		
			322 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			322 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright (c) Edgeless Systems GmbH
 | |
| 
 | |
| SPDX-License-Identifier: AGPL-3.0-only
 | |
| */
 | |
| 
 | |
| // add-version adds a new constellation release version to the list of available versions.
 | |
| // It is meant to be run by the CI pipeline to make new versions available / discoverable.
 | |
| package main
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"errors"
 | |
| 	"flag"
 | |
| 	"fmt"
 | |
| 	"path"
 | |
| 	"sort"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/aws/aws-sdk-go-v2/aws"
 | |
| 	awsconfig "github.com/aws/aws-sdk-go-v2/config"
 | |
| 	s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
 | |
| 	"github.com/aws/aws-sdk-go-v2/service/cloudfront"
 | |
| 	cftypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types"
 | |
| 	"github.com/aws/aws-sdk-go-v2/service/s3"
 | |
| 	s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
 | |
| 	"github.com/edgelesssys/constellation/v2/internal/constants"
 | |
| 	"github.com/edgelesssys/constellation/v2/internal/logger"
 | |
| 	"github.com/edgelesssys/constellation/v2/internal/update"
 | |
| 	"go.uber.org/zap"
 | |
| 	"go.uber.org/zap/zapcore"
 | |
| 	"golang.org/x/mod/semver"
 | |
| )
 | |
| 
 | |
| var errVersionListMissing = errors.New("version list does not exist")
 | |
| 
 | |
| const (
 | |
| 	stream                       = "stable"
 | |
| 	imageKind                    = "image"
 | |
| 	defaultRegion                = "eu-central-1"
 | |
| 	defaultBucket                = "cdn-constellation-backend"
 | |
| 	defaultDistributionID        = "E1H77EZTHC3NE4"
 | |
| 	maxCacheInvalidationWaitTime = 5 * time.Minute
 | |
| )
 | |
| 
 | |
| func main() {
 | |
| 	version := flag.String("version", "", "Version to add (format: \"v1.2.3\")")
 | |
| 	region := flag.String("region", defaultRegion, "AWS region")
 | |
| 	bucket := flag.String("bucket", defaultBucket, "S3 bucket")
 | |
| 	distributionID := flag.String("distribution-id", defaultDistributionID, "cloudfront distribution id")
 | |
| 	flag.Parse()
 | |
| 
 | |
| 	log := logger.New(logger.JSONLog, zapcore.InfoLevel)
 | |
| 	if err := validateVersion(*version); err != nil {
 | |
| 		log.With(zap.Error(err)).Fatalf("Invalid version")
 | |
| 	}
 | |
| 	major := semver.Major(*version)
 | |
| 	minor := semver.MajorMinor(*version)
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 
 | |
| 	updateFetcher := update.New()
 | |
| 	versionManager, err := newVersionManager(ctx, *region, *bucket, *distributionID)
 | |
| 	if err != nil {
 | |
| 		log.With(zap.Error(err)).Fatalf("Failed to create version uploader")
 | |
| 	}
 | |
| 
 | |
| 	// ensure minor version exists in list for base major version
 | |
| 	minorVersions, err := versionManager.getMinorVersions(ctx, *version)
 | |
| 	if err != nil {
 | |
| 		if !errors.Is(err, errVersionListMissing) {
 | |
| 			log.With(zap.Error(err)).Fatalf("Failed to get minor versions")
 | |
| 		}
 | |
| 		log.Infof("Version list for minor versions under %q does not exist. Creating new list.", major)
 | |
| 		minorVersions = &update.VersionsList{
 | |
| 			Stream:      stream,
 | |
| 			Granularity: "major",
 | |
| 			Base:        major,
 | |
| 			Kind:        imageKind,
 | |
| 			Versions:    []string{},
 | |
| 		}
 | |
| 	}
 | |
| 	if minorVersions.Contains(minor) {
 | |
| 		log.Infof("Version %q already exists in list %v.", minor, minorVersions.Versions)
 | |
| 	} else {
 | |
| 		if err := versionManager.addMinorVersion(ctx, *version, minorVersions); err != nil {
 | |
| 			log.With(zap.Error(err)).Fatalf("Failed to add minor version")
 | |
| 		}
 | |
| 		log.Infof("Added %q to list.", minor)
 | |
| 	}
 | |
| 
 | |
| 	// ensure patch version exists in list for base minor version
 | |
| 	patchVersions, err := versionManager.getPatchVersions(ctx, *version)
 | |
| 	if err != nil {
 | |
| 		if !errors.Is(err, errVersionListMissing) {
 | |
| 			log.With(zap.Error(err)).Fatalf("Failed to get patch versions")
 | |
| 		}
 | |
| 		log.Infof("Version list for patch versions under %q does not exist. Creating new list.", minor)
 | |
| 		patchVersions = &update.VersionsList{
 | |
| 			Stream:      stream,
 | |
| 			Granularity: "minor",
 | |
| 			Base:        minor,
 | |
| 			Kind:        imageKind,
 | |
| 			Versions:    []string{},
 | |
| 		}
 | |
| 	}
 | |
| 	if patchVersions.Contains(*version) {
 | |
| 		log.Infof("Version %q already exists in list %v.", *version, patchVersions.Versions)
 | |
| 	} else {
 | |
| 		if err := versionManager.addPatchVersion(ctx, *version, patchVersions); err != nil {
 | |
| 			log.With(zap.Error(err)).Fatalf("Failed to add patch version")
 | |
| 		}
 | |
| 		log.Infof("Added %q to list.", *version)
 | |
| 	}
 | |
| 
 | |
| 	log.Infof("Successfully added version %q at the following URLs:", *version)
 | |
| 	log.Infof("major to minor url: %s", versionURL("major", major))
 | |
| 	log.Infof("minor to patch url: %s", versionURL("minor", minor))
 | |
| 
 | |
| 	log.Infof("Waiting for cache invalidation.")
 | |
| 	if err := versionManager.invalidateCaches(ctx, *version); err != nil {
 | |
| 		log.With(zap.Error(err)).Fatalf("Failed to invalidate caches")
 | |
| 	}
 | |
| 
 | |
| 	sawAddedVersions := true
 | |
| 	if err := ensureMinorVersionExists(ctx, updateFetcher, *version); err != nil {
 | |
| 		sawAddedVersions = false
 | |
| 		log.Warnf("Failed to ensure minor version exists: %v. This may be resolved by waiting.", err)
 | |
| 	}
 | |
| 
 | |
| 	if err := ensurePatchVersionExists(ctx, updateFetcher, *version); err != nil {
 | |
| 		sawAddedVersions = false
 | |
| 		log.Warnf("Failed to ensure patch version exists: %v. This may be resolved by waiting.", err)
 | |
| 	}
 | |
| 
 | |
| 	if sawAddedVersions {
 | |
| 		log.Infof("Versions are available via API.")
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func validateVersion(version string) error {
 | |
| 	if !semver.IsValid(version) {
 | |
| 		return fmt.Errorf("version %q is not a valid semantic version", version)
 | |
| 	}
 | |
| 	if semver.Canonical(version) != version {
 | |
| 		return fmt.Errorf("version %q is not a canonical semantic version", version)
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func ensureMinorVersionExists(ctx context.Context, fetcher *update.VersionsFetcher, version string) error {
 | |
| 	major := semver.Major(version)
 | |
| 	minor := semver.MajorMinor(version)
 | |
| 	existingMinorVersions, err := fetcher.MinorVersionsOf(ctx, stream, major, imageKind)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if !existingMinorVersions.Contains(minor) {
 | |
| 		return errors.New("minor version does not exist")
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func ensurePatchVersionExists(ctx context.Context, fetcher *update.VersionsFetcher, version string) error {
 | |
| 	minor := semver.MajorMinor(version)
 | |
| 	patch := semver.Canonical(version)
 | |
| 	existingPatchVersions, err := fetcher.PatchVersionsOf(ctx, stream, minor, imageKind)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if !existingPatchVersions.Contains(patch) {
 | |
| 		return errors.New("patch version does not exist")
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| type versionManager struct {
 | |
| 	config         aws.Config
 | |
| 	cloudfrontc    *cloudfront.Client
 | |
| 	s3c            *s3.Client
 | |
| 	uploader       *s3manager.Uploader
 | |
| 	bucket         string
 | |
| 	distributionID string
 | |
| }
 | |
| 
 | |
| func newVersionManager(ctx context.Context, region, bucket, distributionID string) (*versionManager, error) {
 | |
| 	cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	cloudfrontc := cloudfront.NewFromConfig(cfg)
 | |
| 	s3c := s3.NewFromConfig(cfg)
 | |
| 	uploader := s3manager.NewUploader(s3c)
 | |
| 	return &versionManager{
 | |
| 		config:         cfg,
 | |
| 		cloudfrontc:    cloudfrontc,
 | |
| 		s3c:            s3c,
 | |
| 		uploader:       uploader,
 | |
| 		bucket:         bucket,
 | |
| 		distributionID: distributionID,
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| func (m *versionManager) getMinorVersions(ctx context.Context, version string) (*update.VersionsList, error) {
 | |
| 	baseVersion := semver.Major(version)
 | |
| 	return m.getVersions(ctx, baseVersion)
 | |
| }
 | |
| 
 | |
| func (m *versionManager) getPatchVersions(ctx context.Context, version string) (*update.VersionsList, error) {
 | |
| 	baseVersion := semver.MajorMinor(version)
 | |
| 	return m.getVersions(ctx, baseVersion)
 | |
| }
 | |
| 
 | |
| func (m *versionManager) addMinorVersion(ctx context.Context, version string, minorVersions *update.VersionsList) error {
 | |
| 	baseVersion := semver.Major(version)
 | |
| 	minorVersion := semver.MajorMinor(version)
 | |
| 	return m.addVersion(ctx, baseVersion, minorVersion, minorVersions)
 | |
| }
 | |
| 
 | |
| func (m *versionManager) addPatchVersion(ctx context.Context, version string, patchVersions *update.VersionsList) error {
 | |
| 	baseVersion := semver.MajorMinor(version)
 | |
| 	patchVersion := semver.Canonical(version)
 | |
| 	return m.addVersion(ctx, baseVersion, patchVersion, patchVersions)
 | |
| }
 | |
| 
 | |
| func (m *versionManager) getVersions(ctx context.Context, baseVersion string) (*update.VersionsList, error) {
 | |
| 	granularity, err := granularityFromVersion(baseVersion)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	out, err := m.s3c.GetObject(ctx, &s3.GetObjectInput{
 | |
| 		Bucket: aws.String(m.bucket),
 | |
| 		Key:    aws.String(versionJSONPath(granularity, baseVersion)),
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		var nosuchkey *s3types.NoSuchKey
 | |
| 		if errors.As(err, &nosuchkey) {
 | |
| 			return nil, errVersionListMissing
 | |
| 		}
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	defer out.Body.Close()
 | |
| 	var versions update.VersionsList
 | |
| 	if err := json.NewDecoder(out.Body).Decode(&versions); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return &versions, nil
 | |
| }
 | |
| 
 | |
| func (m *versionManager) addVersion(ctx context.Context, baseVersion, version string, list *update.VersionsList) error {
 | |
| 	granularity, err := granularityFromVersion(baseVersion)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	list.Versions = append(list.Versions, version)
 | |
| 	sort.Strings(list.Versions)
 | |
| 	if err := list.Validate(); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	rawList, err := json.Marshal(list)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	_, err = m.uploader.Upload(ctx, &s3.PutObjectInput{
 | |
| 		Bucket: aws.String(m.bucket),
 | |
| 		Key:    aws.String(versionJSONPath(granularity, baseVersion)),
 | |
| 		Body:   bytes.NewBuffer(rawList),
 | |
| 	})
 | |
| 	return err
 | |
| }
 | |
| 
 | |
| func (m *versionManager) invalidateCaches(ctx context.Context, version string) error {
 | |
| 	major := semver.Major(version)
 | |
| 	minor := semver.MajorMinor(version)
 | |
| 	invalidation, err := m.cloudfrontc.CreateInvalidation(ctx, &cloudfront.CreateInvalidationInput{
 | |
| 		DistributionId: aws.String(m.distributionID),
 | |
| 		InvalidationBatch: &cftypes.InvalidationBatch{
 | |
| 			CallerReference: aws.String(fmt.Sprintf("%d", time.Now().Unix())),
 | |
| 			Paths: &cftypes.Paths{
 | |
| 				Quantity: aws.Int32(2),
 | |
| 				Items: []string{
 | |
| 					"/" + versionJSONPath("major", major),
 | |
| 					"/" + versionJSONPath("minor", minor),
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	})
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	waiter := cloudfront.NewInvalidationCompletedWaiter(m.cloudfrontc)
 | |
| 	if err := waiter.Wait(ctx, &cloudfront.GetInvalidationInput{
 | |
| 		DistributionId: aws.String(m.distributionID),
 | |
| 		Id:             invalidation.Invalidation.Id,
 | |
| 	}, maxCacheInvalidationWaitTime); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func granularityFromVersion(version string) (string, error) {
 | |
| 	switch {
 | |
| 	case semver.Major(version) == version:
 | |
| 		return "major", nil
 | |
| 	case semver.MajorMinor(version) == version:
 | |
| 		return "minor", nil
 | |
| 	case semver.Canonical(version) == version:
 | |
| 		return "patch", nil
 | |
| 	default:
 | |
| 		return "", fmt.Errorf("invalid version %q", version)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func versionJSONPath(granularity, base string) string {
 | |
| 	return path.Join(constants.CDNVersionsPath, "stream", stream, granularity, base, imageKind+".json")
 | |
| }
 | |
| 
 | |
| func versionURL(granularity, base string) string {
 | |
| 	return constants.CDNRepositoryURL + "/" + versionJSONPath(granularity, base)
 | |
| }
 |