ci: use aws s3 client that invalidates cloudfront cache for places that modify Constellation api (#1839)

This commit is contained in:
Malte Poll 2023-06-02 11:20:01 +02:00 committed by GitHub
parent 93569ff54c
commit e1d3afe8d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 398 additions and 186 deletions

View File

@ -50,12 +50,12 @@ func main() {
cliInfo.Kubernetes = append(cliInfo.Kubernetes, v.ClusterVersion)
}
c, err := client.NewClient(ctx, "eu-central-1", "cdn-constellation-backend", "E1H77EZTHC3NE4", false, log)
c, cclose, err := client.NewClient(ctx, "eu-central-1", "cdn-constellation-backend", "E1H77EZTHC3NE4", false, log)
if err != nil {
log.Fatalf("creating s3 client: %w", err)
}
defer func() {
if err := c.InvalidateCache(ctx); err != nil {
if err := cclose(ctx); err != nil {
log.Fatalf("invalidating cache: %w", err)
}
}()

View File

@ -76,10 +76,15 @@ func runCmd(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("unmarshalling version file: %w", err)
}
sut, err := attestationconfigclient.New(ctx, cfg, []byte(cosignPwd), privateKey)
sut, sutClose, err := attestationconfigclient.New(ctx, cfg, []byte(cosignPwd), privateKey)
if err != nil {
return fmt.Errorf("creating repo: %w", err)
}
defer func() {
if err := sutClose(ctx); err != nil {
fmt.Printf("closing repo: %v\n", err)
}
}()
if err := sut.UploadAzureSEVSNP(ctx, versions, time.Now()); err != nil {
return fmt.Errorf("uploading version: %w", err)

View File

@ -46,10 +46,15 @@ func runAWS(cmd *cobra.Command, _ []string) error {
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
archiveC, err := archive.New(cmd.Context(), flags.region, flags.bucket, log)
archiveC, archiveCClose, err := archive.New(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
if err != nil {
return err
}
defer func() {
if err := archiveCClose(cmd.Context()); err != nil {
log.Errorf("closing archive client: %v", err)
}
}()
uploadC, err := awsupload.New(flags.awsRegion, flags.awsBucket, log)
if err != nil {

View File

@ -47,10 +47,15 @@ func runAzure(cmd *cobra.Command, _ []string) error {
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
archiveC, err := archive.New(cmd.Context(), flags.region, flags.bucket, log)
archiveC, archiveCClose, err := archive.New(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
if err != nil {
return err
}
defer func() {
if err := archiveCClose(cmd.Context()); err != nil {
log.Errorf("closing archive client: %v", err)
}
}()
uploadC, err := azureupload.New(flags.azSubscription, flags.azLocation, flags.azResourceGroup, log)
if err != nil {

View File

@ -27,6 +27,7 @@ type commonFlags struct {
timestamp time.Time
region string
bucket string
distributionID string
out string
logLevel zapcore.Level
}
@ -75,6 +76,10 @@ func parseCommonFlags(cmd *cobra.Command) (commonFlags, error) {
if err != nil {
return commonFlags{}, err
}
distributionID, err := cmd.Flags().GetString("distribution-id")
if err != nil {
return commonFlags{}, err
}
out, err := cmd.Flags().GetString("out")
if err != nil {
return commonFlags{}, err
@ -96,6 +101,7 @@ func parseCommonFlags(cmd *cobra.Command) (commonFlags, error) {
timestamp: timestmp,
region: region,
bucket: bucket,
distributionID: distributionID,
out: out,
logLevel: logLevel,
}, nil
@ -201,9 +207,10 @@ func parseGCPFlags(cmd *cobra.Command) (gcpFlags, error) {
}
type s3Flags struct {
region string
bucket string
logLevel zapcore.Level
region string
bucket string
distributionID string
logLevel zapcore.Level
}
func parseS3Flags(cmd *cobra.Command) (s3Flags, error) {
@ -215,6 +222,10 @@ func parseS3Flags(cmd *cobra.Command) (s3Flags, error) {
if err != nil {
return s3Flags{}, err
}
distributionID, err := cmd.Flags().GetString("distribution-id")
if err != nil {
return s3Flags{}, err
}
verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return s3Flags{}, err
@ -225,9 +236,10 @@ func parseS3Flags(cmd *cobra.Command) (s3Flags, error) {
}
return s3Flags{
region: region,
bucket: bucket,
logLevel: logLevel,
region: region,
bucket: bucket,
distributionID: distributionID,
logLevel: logLevel,
}, nil
}

View File

@ -47,10 +47,15 @@ func runGCP(cmd *cobra.Command, _ []string) error {
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
archiveC, err := archive.New(cmd.Context(), flags.region, flags.bucket, log)
archiveC, archiveCClose, err := archive.New(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
if err != nil {
return err
}
defer func() {
if err := archiveCClose(cmd.Context()); err != nil {
log.Errorf("closing archive client: %v", err)
}
}()
uploadC, err := gcpupload.New(cmd.Context(), flags.gcpProject, flags.gcpLocation, flags.gcpBucket, log)
if err != nil {

View File

@ -31,6 +31,7 @@ func NewImageCmd() *cobra.Command {
cmd.PersistentFlags().String("timestamp", "", "Optional timestamp to use for resource names. Uses format 2006-01-02T15:04:05Z07:00.")
cmd.PersistentFlags().String("region", "eu-central-1", "AWS region of the archive S3 bucket")
cmd.PersistentFlags().String("bucket", "cdn-constellation-backend", "S3 bucket name of the archive")
cmd.PersistentFlags().String("distribution-id", "E1H77EZTHC3NE4", "CloudFront distribution ID of the API")
cmd.PersistentFlags().String("out", "", "Optional path to write the upload result to. If not set, the result is written to stdout.")
cmd.PersistentFlags().Bool("verbose", false, "Enable verbose output")
must(cmd.MarkPersistentFlagRequired("raw-image"))

View File

@ -31,6 +31,7 @@ func NewInfoCmd() *cobra.Command {
cmd.Flags().String("region", "eu-central-1", "AWS region of the archive S3 bucket")
cmd.Flags().String("bucket", "cdn-constellation-backend", "S3 bucket name of the archive")
cmd.Flags().String("distribution-id", "E1H77EZTHC3NE4", "CloudFront distribution ID of the API")
cmd.Flags().Bool("verbose", false, "Enable verbose output")
return cmd
@ -54,10 +55,15 @@ func runInfo(cmd *cobra.Command, args []string) error {
return err
}
uploadC, err := infoupload.New(cmd.Context(), flags.region, flags.bucket, log)
uploadC, uploadCClose, err := infoupload.New(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
if err != nil {
return fmt.Errorf("uploading image info: %w", err)
}
defer func() {
if err := uploadCClose(cmd.Context()); err != nil {
log.Errorf("closing upload client: %v", err)
}
}()
url, err := uploadC.Upload(cmd.Context(), info)
if err != nil {

View File

@ -31,6 +31,7 @@ func newMeasurementsUploadCmd() *cobra.Command {
cmd.Flags().String("signature", "", "Path to signature file to upload")
cmd.Flags().String("region", "eu-central-1", "AWS region of the archive S3 bucket")
cmd.Flags().String("bucket", "cdn-constellation-backend", "S3 bucket name of the archive")
cmd.Flags().String("distribution-id", "E1H77EZTHC3NE4", "CloudFront distribution ID of the API")
cmd.Flags().Bool("verbose", false, "Enable verbose output")
must(cmd.MarkFlagRequired("measurements"))
@ -53,10 +54,15 @@ func runMeasurementsUpload(cmd *cobra.Command, _ []string) error {
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
uploadC, err := measurementsuploader.New(cmd.Context(), flags.region, flags.bucket, log)
uploadC, uploadCClose, err := measurementsuploader.New(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
if err != nil {
return fmt.Errorf("uploading image info: %w", err)
}
defer func() {
if err := uploadCClose(cmd.Context()); err != nil {
log.Errorf("closing upload client: %v", err)
}
}()
measurements, err := os.Open(flags.measurementsPath)
if err != nil {

View File

@ -33,10 +33,15 @@ func runNOP(cmd *cobra.Command, provider cloudprovider.Provider, _ []string) err
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
archiveC, err := archive.New(cmd.Context(), flags.region, flags.bucket, log)
archiveC, archiveCClose, err := archive.New(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
if err != nil {
return err
}
defer func() {
if err := archiveCClose(cmd.Context()); err != nil {
log.Errorf("closing archive client: %v", err)
}
}()
uploadC := nopupload.New(log)

View File

@ -13,6 +13,7 @@ go_library(
"//internal/sigstore",
"//internal/staticupload",
"//internal/variant",
"@com_github_aws_aws_sdk_go_v2_feature_s3_manager//:manager",
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
],
)

View File

@ -16,6 +16,7 @@ import (
"sort"
"time"
s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
"github.com/edgelesssys/constellation/v2/internal/constants"
@ -27,18 +28,38 @@ import (
// Client manages (modifies) the version information for the attestation variants.
type Client struct {
*staticupload.Client
cosignPwd []byte // used to decrypt the cosign private key
privKey []byte // used to sign
s3Client
s3ClientClose func(ctx context.Context) error
bucketID string
cosignPwd []byte // used to decrypt the cosign private key
privKey []byte // used to sign
}
// New returns a new Client.
func New(ctx context.Context, cfg staticupload.Config, cosignPwd, privateKey []byte) (*Client, error) {
client, err := staticupload.New(ctx, cfg)
func New(ctx context.Context, cfg staticupload.Config, cosignPwd, privateKey []byte) (*Client, CloseFunc, error) {
client, clientClose, err := staticupload.New(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("failed to create s3 storage: %w", err)
return nil, nil, fmt.Errorf("failed to create s3 storage: %w", err)
}
return &Client{client, cosignPwd, privateKey}, nil
repo := &Client{
s3Client: client,
s3ClientClose: clientClose,
bucketID: cfg.Bucket,
cosignPwd: cosignPwd,
privKey: privateKey,
}
repoClose := func(ctx context.Context) error {
return repo.Close(ctx)
}
return repo, repoClose, nil
}
// Close closes the Client.
func (a Client) Close(ctx context.Context) error {
if a.s3ClientClose == nil {
return nil
}
return a.s3ClientClose(ctx)
}
// UploadAzureSEVSNP uploads the latest version numbers of the Azure SEVSNP.
@ -51,7 +72,7 @@ func (a Client) UploadAzureSEVSNP(ctx context.Context, versions attestationconfi
fname := date.Format("2006-01-02-15-04") + ".json"
filePath := fmt.Sprintf("%s/%s/%s", constants.CDNAttestationConfigPrefixV1, variant.String(), fname)
err = put(ctx, a.Client, filePath, versionBytes)
err = put(ctx, a.s3Client, a.bucketID, filePath, versionBytes)
if err != nil {
return err
}
@ -69,7 +90,7 @@ func (a Client) createAndUploadSignature(ctx context.Context, content []byte, fi
if err != nil {
return fmt.Errorf("sign version file: %w", err)
}
err = put(ctx, a.Client, filePath+".sig", signature)
err = put(ctx, a.s3Client, a.bucketID, filePath+".sig", signature)
if err != nil {
return fmt.Errorf("upload signature: %w", err)
}
@ -79,7 +100,7 @@ func (a Client) createAndUploadSignature(ctx context.Context, content []byte, fi
// List returns the list of versions for the given attestation type.
func (a Client) List(ctx context.Context, attestation variant.Variant) ([]string, error) {
key := path.Join(constants.CDNAttestationConfigPrefixV1, attestation.String(), "list")
bt, err := get(ctx, a.Client, key)
bt, err := get(ctx, a.s3Client, a.bucketID, key)
if err != nil {
return nil, err
}
@ -97,13 +118,13 @@ func (a Client) DeleteList(ctx context.Context, attestation variant.Variant) err
if err != nil {
return err
}
return put(ctx, a.Client, path.Join(constants.CDNAttestationConfigPrefixV1, attestation.String(), "list"), bt)
return put(ctx, a.s3Client, a.bucketID, path.Join(constants.CDNAttestationConfigPrefixV1, attestation.String(), "list"), bt)
}
func (a Client) addVersionToList(ctx context.Context, attestation variant.Variant, fname string) error {
versions := []string{}
key := path.Join(constants.CDNAttestationConfigPrefixV1, attestation.String(), "list")
bt, err := get(ctx, a.Client, key)
bt, err := get(ctx, a.s3Client, a.bucketID, key)
if err == nil {
if err := json.Unmarshal(bt, &versions); err != nil {
return err
@ -118,13 +139,13 @@ func (a Client) addVersionToList(ctx context.Context, attestation variant.Varian
if err != nil {
return err
}
return put(ctx, a.Client, key, json)
return put(ctx, a.s3Client, a.bucketID, key, json)
}
// get is a convenience method.
func get(ctx context.Context, client *staticupload.Client, path string) ([]byte, error) {
func get(ctx context.Context, client s3Client, bucket, path string) ([]byte, error) {
getObjectInput := &s3.GetObjectInput{
Bucket: &client.BucketID,
Bucket: &bucket,
Key: &path,
}
output, err := client.GetObject(ctx, getObjectInput)
@ -135,12 +156,24 @@ func get(ctx context.Context, client *staticupload.Client, path string) ([]byte,
}
// put is a convenience method.
func put(ctx context.Context, client *staticupload.Client, path string, data []byte) error {
func put(ctx context.Context, client s3Client, bucket, path string, data []byte) error {
putObjectInput := &s3.PutObjectInput{
Bucket: &client.BucketID,
Bucket: &bucket,
Key: &path,
Body: bytes.NewReader(data),
}
_, err := client.Upload(ctx, putObjectInput)
return err
}
type s3Client interface {
GetObject(
ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options),
) (*s3.GetObjectOutput, error)
Upload(
ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader),
) (*s3manager.UploadOutput, error)
}
// CloseFunc is a function that closes the client.
type CloseFunc func(ctx context.Context) error

View File

@ -74,8 +74,9 @@ var versionValues = attestationconfig.AzureSEVSNPVersion{
func TestUploadAzureSEVSNPVersions(t *testing.T) {
ctx := context.Background()
client, err := client.New(ctx, cfg, []byte(*cosignPwd), privateKey)
client, clientClose, err := client.New(ctx, cfg, []byte(*cosignPwd), privateKey)
require.NoError(t, err)
defer func() { _ = clientClose(ctx) }()
d := time.Date(2021, 1, 1, 1, 1, 1, 1, time.UTC)
require.NoError(t, client.UploadAzureSEVSNP(ctx, versionValues, d))
}

View File

@ -7,11 +7,8 @@ go_library(
visibility = ["//:__subpackages__"],
deps = [
"//internal/logger",
"@com_github_aws_aws_sdk_go_v2//aws",
"@com_github_aws_aws_sdk_go_v2_config//:config",
"//internal/staticupload",
"@com_github_aws_aws_sdk_go_v2_feature_s3_manager//:manager",
"@com_github_aws_aws_sdk_go_v2_service_cloudfront//:cloudfront",
"@com_github_aws_aws_sdk_go_v2_service_cloudfront//types",
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
"@com_github_aws_aws_sdk_go_v2_service_s3//types",
"@org_uber_go_zap//:zap",

View File

@ -35,25 +35,20 @@ import (
"fmt"
"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/logger"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
"go.uber.org/zap"
)
// Client is the client for the versions API.
type Client struct {
config aws.Config
cloudfrontClient *cloudfront.Client
s3Client *s3.Client
uploadClient *s3manager.Uploader
uploadClient uploadClient
s3Client s3Client
s3ClientClose func(ctx context.Context) error
bucket string
distributionID string
cacheInvalidationWaitTimeout time.Duration
dirtyPaths []string // written paths to be invalidated
@ -66,90 +61,68 @@ type Client struct {
// This client can be used to fetch objects but cannot write updates.
func NewReadOnlyClient(ctx context.Context, region, bucket, distributionID string,
log *logger.Logger,
) (*Client, error) {
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
) (*Client, CloseFunc, error) {
staticUploadClient, staticUploadClientClose, err := staticupload.New(ctx, staticupload.Config{
Region: region,
Bucket: bucket,
DistributionID: distributionID,
CacheInvalidationStrategy: staticupload.CacheInvalidateBatchOnFlush,
})
if err != nil {
return nil, err
return nil, nil, err
}
s3c := s3.NewFromConfig(cfg)
return &Client{
config: cfg,
s3Client: s3c,
bucket: bucket,
distributionID: distributionID,
DryRun: true,
Log: log,
}, nil
client := &Client{
s3Client: staticUploadClient,
s3ClientClose: staticUploadClientClose,
bucket: bucket,
DryRun: true,
Log: log,
}
clientClose := func(ctx context.Context) error {
return client.Close(ctx)
}
return client, clientClose, nil
}
// NewClient creates a new client for the versions API.
func NewClient(ctx context.Context, region, bucket, distributionID string, dryRun bool,
log *logger.Logger,
) (*Client, error) {
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
) (*Client, CloseFunc, error) {
staticUploadClient, staticUploadClientClose, err := staticupload.New(ctx, staticupload.Config{
Region: region,
Bucket: bucket,
DistributionID: distributionID,
CacheInvalidationStrategy: staticupload.CacheInvalidateBatchOnFlush,
})
if err != nil {
return nil, err
return nil, nil, err
}
cloudfrontC := cloudfront.NewFromConfig(cfg)
s3C := s3.NewFromConfig(cfg)
uploadC := s3manager.NewUploader(s3C)
return &Client{
config: cfg,
cloudfrontClient: cloudfrontC,
s3Client: s3C,
uploadClient: uploadC,
client := &Client{
uploadClient: staticUploadClient,
s3Client: staticUploadClient,
s3ClientClose: staticUploadClientClose,
bucket: bucket,
distributionID: distributionID,
DryRun: dryRun,
Log: log,
cacheInvalidationWaitTimeout: 10 * time.Minute,
}, nil
}
clientClose := func(ctx context.Context) error {
return client.Close(ctx)
}
return client, clientClose, nil
}
// InvalidateCache invalidates the CDN cache for the paths that have been written.
// The function should be deferred after the client has been created.
func (c *Client) InvalidateCache(ctx context.Context) error {
if len(c.dirtyPaths) == 0 {
c.Log.Debugf("No dirty paths, skipping cache invalidation")
// Close closes the client.
// It invalidates the CDN cache for all uploaded files.
func (c *Client) Close(ctx context.Context) error {
if c.s3ClientClose == nil {
return nil
}
if c.DryRun {
c.Log.With(zap.String("distributionID", c.distributionID), zap.Strings("dirtyPaths", c.dirtyPaths)).Debugf("DryRun: cloudfront create invalidation")
return nil
}
c.Log.Debugf("Paths to invalidate: %v", c.dirtyPaths)
in := &cloudfront.CreateInvalidationInput{
DistributionId: &c.distributionID,
InvalidationBatch: &cftypes.InvalidationBatch{
CallerReference: ptr(fmt.Sprintf("%d", time.Now().Unix())),
Paths: &cftypes.Paths{
Items: c.dirtyPaths,
Quantity: ptr(int32(len(c.dirtyPaths))),
},
},
}
invalidation, err := c.cloudfrontClient.CreateInvalidation(ctx, in)
if err != nil {
return fmt.Errorf("creating invalidation: %w", err)
}
c.Log.Debugf("Waiting for invalidation %s to complete", *invalidation.Invalidation.Id)
waiter := cloudfront.NewInvalidationCompletedWaiter(c.cloudfrontClient)
waitIn := &cloudfront.GetInvalidationInput{
DistributionId: &c.distributionID,
Id: invalidation.Invalidation.Id,
}
if err := waiter.Wait(ctx, waitIn, c.cacheInvalidationWaitTimeout); err != nil {
return fmt.Errorf("waiting for invalidation to complete: %w", err)
}
return nil
return c.s3ClientClose(ctx)
}
// DeletePath deletes all objects at a given path from a s3 bucket.
@ -289,3 +262,22 @@ func (e *NotFoundError) Error() string {
func (e *NotFoundError) Unwrap() error {
return e.err
}
type s3Client interface {
GetObject(
ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options),
) (*s3.GetObjectOutput, error)
ListObjectsV2(
ctx context.Context, params *s3.ListObjectsV2Input, optFns ...func(*s3.Options),
) (*s3.ListObjectsV2Output, error)
DeleteObjects(
ctx context.Context, params *s3.DeleteObjectsInput, optFns ...func(*s3.Options),
) (*s3.DeleteObjectsOutput, error)
}
type uploadClient interface {
Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error)
}
// CloseFunc is a function that closes the client.
type CloseFunc func(ctx context.Context) error

View File

@ -73,13 +73,13 @@ func runAdd(cmd *cobra.Command, _ []string) (retErr error) {
}
log.Debugf("Creating versions API client")
client, err := verclient.NewClient(cmd.Context(), flags.region, flags.bucket, flags.distributionID, flags.dryRun, log)
client, clientClose, 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 {
if err := clientClose(cmd.Context()); err != nil && retErr == nil {
*retErr = fmt.Errorf("invalidating cache: %w", err)
}
}(&retErr)

View File

@ -47,10 +47,15 @@ func runLatest(cmd *cobra.Command, _ []string) error {
}
log.Debugf("Creating versions API client")
client, err := verclient.NewReadOnlyClient(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
client, clientClose, err := verclient.NewReadOnlyClient(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
defer func() {
if err := clientClose(cmd.Context()); err != nil {
log.Errorf("Closing versions API client: %v", err)
}
}()
log.Debugf("Requesting latest version")
latest := versionsapi.Latest{

View File

@ -53,10 +53,15 @@ func runList(cmd *cobra.Command, _ []string) error {
}
log.Debugf("Creating versions API client")
client, err := verclient.NewReadOnlyClient(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
client, clientClose, err := verclient.NewReadOnlyClient(cmd.Context(), flags.region, flags.bucket, flags.distributionID, log)
if err != nil {
return fmt.Errorf("creating client: %w", err)
}
defer func() {
if err := clientClose(cmd.Context()); err != nil {
log.Errorf("Closing versions API client: %v", err)
}
}()
var minorVersions []string
if flags.minorVersion != "" {

View File

@ -102,13 +102,13 @@ func runRemove(cmd *cobra.Command, _ []string) (retErr error) {
}
log.Debugf("Creating versions API client")
verclient, err := verclient.NewClient(cmd.Context(), flags.region, flags.bucket, flags.distributionID, flags.dryrun, log)
verclient, verclientClose, 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 := verclient.InvalidateCache(cmd.Context()); err != nil && retErr == nil {
if err := verclientClose(cmd.Context()); err != nil && retErr == nil {
*retErr = fmt.Errorf("invalidating cache: %w", err)
}
}(&retErr)

View File

@ -27,23 +27,49 @@ import (
// VersionsClient is a client for the versions API.
type VersionsClient struct {
*apiclient.Client
clientClose func(ctx context.Context) error
}
// NewClient creates a new client for the versions API.
func NewClient(ctx context.Context, region, bucket, distributionID string, dryRun bool,
log *logger.Logger,
) (*VersionsClient, error) {
genericClient, err := apiclient.NewClient(ctx, region, bucket, distributionID, dryRun, log)
return &VersionsClient{genericClient}, err
) (*VersionsClient, CloseFunc, error) {
genericClient, genericClientClose, err := apiclient.NewClient(ctx, region, bucket, distributionID, dryRun, log)
versionsClient := &VersionsClient{
genericClient,
genericClientClose,
}
versionsClientClose := func(ctx context.Context) error {
return versionsClient.Close(ctx)
}
return versionsClient, versionsClientClose, err
}
// NewReadOnlyClient creates a new read-only client.
// This client can be used to fetch objects but cannot write updates.
func NewReadOnlyClient(ctx context.Context, region, bucket, distributionID string,
log *logger.Logger,
) (*VersionsClient, error) {
genericClient, err := apiclient.NewReadOnlyClient(ctx, region, bucket, distributionID, log)
return &VersionsClient{genericClient}, err
) (*VersionsClient, CloseFunc, error) {
genericClient, genericClientClose, err := apiclient.NewReadOnlyClient(ctx, region, bucket, distributionID, log)
if err != nil {
return nil, nil, err
}
versionsClient := &VersionsClient{
genericClient,
genericClientClose,
}
versionsClientClose := func(ctx context.Context) error {
return versionsClient.Close(ctx)
}
return versionsClient, versionsClientClose, err
}
// Close closes the client.
func (c *VersionsClient) Close(ctx context.Context) error {
if c.clientClose == nil {
return nil
}
return c.clientClose(ctx)
}
// FetchVersionList fetches the given version list from the versions API.
@ -228,3 +254,6 @@ func (c *VersionsClient) deleteVersionFromLatest(ctx context.Context, ver versio
return nil
}
// CloseFunc is a function that closes the client.
type CloseFunc func(ctx context.Context) error

View File

@ -9,7 +9,7 @@ go_library(
"//internal/api/versions",
"//internal/constants",
"//internal/logger",
"@com_github_aws_aws_sdk_go_v2_config//:config",
"//internal/staticupload",
"@com_github_aws_aws_sdk_go_v2_feature_s3_manager//:manager",
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
"@com_github_aws_aws_sdk_go_v2_service_s3//types",

View File

@ -12,18 +12,19 @@ import (
"io"
"net/url"
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/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
versionsapi "github.com/edgelesssys/constellation/v2/internal/api/versions"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
)
// Archivist uploads OS images to S3.
type Archivist struct {
uploadClient uploadClient
uploadClient uploadClient
uploadClientClose func(ctx context.Context) error
// bucket is the name of the S3 bucket to use.
bucket string
@ -31,19 +32,37 @@ type Archivist struct {
}
// New creates a new Archivist.
func New(ctx context.Context, region, bucket string, log *logger.Logger) (*Archivist, error) {
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
func New(ctx context.Context, region, bucket, distributionID string, log *logger.Logger) (*Archivist, CloseFunc, error) {
staticUploadClient, staticUploadClientClose, err := staticupload.New(ctx, staticupload.Config{
Region: region,
Bucket: bucket,
DistributionID: distributionID,
CacheInvalidationStrategy: staticupload.CacheInvalidateBatchOnFlush,
})
if err != nil {
return nil, err
return nil, nil, err
}
s3client := s3.NewFromConfig(cfg)
uploadClient := s3manager.NewUploader(s3client)
return &Archivist{
uploadClient: uploadClient,
bucket: bucket,
log: log,
}, nil
archivist := &Archivist{
uploadClient: staticUploadClient,
uploadClientClose: staticUploadClientClose,
bucket: bucket,
log: log,
}
archivistClose := func(ctx context.Context) error {
return archivist.Close(ctx)
}
return archivist, archivistClose, nil
}
// Close closes the uploader.
// It invalidates the CDN cache for all uploaded files.
func (a *Archivist) Close(ctx context.Context) error {
if a.uploadClientClose == nil {
return nil
}
return a.uploadClientClose(ctx)
}
// Archive reads the OS image in img and uploads it as key.
@ -65,3 +84,6 @@ func (a *Archivist) Archive(ctx context.Context, version versionsapi.Version, cs
type uploadClient interface {
Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error)
}
// CloseFunc is a function that closes the client.
type CloseFunc func(ctx context.Context) error

View File

@ -9,7 +9,7 @@ go_library(
"//internal/api/versions",
"//internal/constants",
"//internal/logger",
"@com_github_aws_aws_sdk_go_v2_config//:config",
"//internal/staticupload",
"@com_github_aws_aws_sdk_go_v2_feature_s3_manager//:manager",
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
"@com_github_aws_aws_sdk_go_v2_service_s3//types",

View File

@ -13,18 +13,19 @@ import (
"encoding/json"
"net/url"
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/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
versionsapi "github.com/edgelesssys/constellation/v2/internal/api/versions"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
)
// Uploader uploads image info to S3.
type Uploader struct {
uploadClient uploadClient
uploadClient uploadClient
uploadClientClose func(ctx context.Context) error
// bucket is the name of the S3 bucket to use.
bucket string
@ -32,19 +33,36 @@ type Uploader struct {
}
// New creates a new Uploader.
func New(ctx context.Context, region, bucket string, log *logger.Logger) (*Uploader, error) {
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
func New(ctx context.Context, region, bucket, distributionID string, log *logger.Logger) (*Uploader, CloseFunc, error) {
staticUploadClient, staticUploadClientClose, err := staticupload.New(ctx, staticupload.Config{
Region: region,
Bucket: bucket,
DistributionID: distributionID,
CacheInvalidationStrategy: staticupload.CacheInvalidateBatchOnFlush,
})
if err != nil {
return nil, err
return nil, nil, err
}
s3client := s3.NewFromConfig(cfg)
uploadClient := s3manager.NewUploader(s3client)
return &Uploader{
uploadClient: uploadClient,
bucket: bucket,
log: log,
}, nil
uploader := &Uploader{
uploadClient: staticUploadClient,
uploadClientClose: staticUploadClientClose,
bucket: bucket,
log: log,
}
uploaderClose := func(ctx context.Context) error {
return uploader.Close(ctx)
}
return uploader, uploaderClose, nil
}
// Close closes the uploader.
// It invalidates the CDN cache for all uploaded files.
func (a *Uploader) Close(ctx context.Context) error {
if a.uploadClientClose == nil {
return nil
}
return a.uploadClientClose(ctx)
}
// Upload marshals the image info to JSON and uploads it to S3.
@ -76,3 +94,6 @@ func (a *Uploader) Upload(ctx context.Context, imageInfo versionsapi.ImageInfo)
type uploadClient interface {
Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error)
}
// CloseFunc is a function that closes the client.
type CloseFunc func(ctx context.Context) error

View File

@ -10,7 +10,7 @@ go_library(
"//internal/attestation/measurements",
"//internal/constants",
"//internal/logger",
"@com_github_aws_aws_sdk_go_v2_config//:config",
"//internal/staticupload",
"@com_github_aws_aws_sdk_go_v2_feature_s3_manager//:manager",
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
"@com_github_aws_aws_sdk_go_v2_service_s3//types",

View File

@ -14,7 +14,6 @@ import (
"io"
"net/url"
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/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
@ -22,11 +21,13 @@ import (
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
)
// Uploader uploads image info to S3.
type Uploader struct {
uploadClient uploadClient
uploadClient uploadClient
uploadClientClose func(ctx context.Context) error
// bucket is the name of the S3 bucket to use.
bucket string
@ -34,19 +35,36 @@ type Uploader struct {
}
// New creates a new Uploader.
func New(ctx context.Context, region, bucket string, log *logger.Logger) (*Uploader, error) {
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(region))
func New(ctx context.Context, region, bucket, distributionID string, log *logger.Logger) (*Uploader, CloseFunc, error) {
staticUploadClient, staticUploadClientClose, err := staticupload.New(ctx, staticupload.Config{
Region: region,
Bucket: bucket,
DistributionID: distributionID,
CacheInvalidationStrategy: staticupload.CacheInvalidateBatchOnFlush,
})
if err != nil {
return nil, err
return nil, nil, err
}
s3client := s3.NewFromConfig(cfg)
uploadClient := s3manager.NewUploader(s3client)
return &Uploader{
uploadClient: uploadClient,
bucket: bucket,
log: log,
}, nil
uploader := &Uploader{
uploadClient: staticUploadClient,
uploadClientClose: staticUploadClientClose,
bucket: bucket,
log: log,
}
uploaderClose := func(ctx context.Context) error {
return uploader.Close(ctx)
}
return uploader, uploaderClose, nil
}
// Close closes the uploader.
// It invalidates the CDN cache for all uploaded files.
func (a *Uploader) Close(ctx context.Context) error {
if a.uploadClientClose == nil {
return nil
}
return a.uploadClientClose(ctx)
}
// Upload uploads the measurements v2 JSON file and its signature to S3.
@ -97,3 +115,6 @@ func (a *Uploader) Upload(ctx context.Context, rawMeasurement, signature io.Read
type uploadClient interface {
Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error)
}
// CloseFunc is a function that closes the client.
type CloseFunc func(ctx context.Context) error

View File

@ -12,7 +12,16 @@ import (
"github.com/aws/aws-sdk-go-v2/service/s3"
)
// GetObject returns an object from from AWS S3 Storage.
func (s *Client) GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
return s.s3Client.GetObject(ctx, params, optFns...)
// GetObject retrieves objects from Amazon S3.
func (c *Client) GetObject(
ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options),
) (*s3.GetObjectOutput, error) {
return c.s3Client.GetObject(ctx, params, optFns...)
}
// ListObjectsV2 returns some or all (up to 1,000) of the objects in a bucket.
func (c *Client) ListObjectsV2(
ctx context.Context, params *s3.ListObjectsV2Input, optFns ...func(*s3.Options),
) (*s3.ListObjectsV2Output, error) {
return c.s3Client.ListObjectsV2(ctx, params, optFns...)
}

View File

@ -36,7 +36,7 @@ type Client struct {
uploadClient uploadClient
s3Client objectStorageClient
distributionID string
BucketID string
bucketID string
cacheInvalidationStrategy CacheInvalidationStrategy
cacheInvalidationWaitTimeout time.Duration
@ -73,9 +73,9 @@ type CacheInvalidationStrategy int
const (
// CacheInvalidateEager invalidates the CDN cache immediately for every key that is uploaded.
CacheInvalidateEager CacheInvalidationStrategy = iota
// CacheInvalidateBatchOnClose invalidates the CDN cache in batches when the client is closed.
// This is useful when uploading many files at once but will fail if Close is not called.
CacheInvalidateBatchOnClose
// CacheInvalidateBatchOnFlush invalidates the CDN cache in batches when the client is flushed / closed.
// This is useful when uploading many files at once but may fail to invalidate the cache if close is not called.
CacheInvalidateBatchOnFlush
)
// InvalidationError is an error that occurs when invalidating the CDN cache.
@ -94,33 +94,39 @@ func (e InvalidationError) Unwrap() error {
}
// New creates a new Client.
func New(ctx context.Context, config Config) (*Client, error) {
func New(ctx context.Context, config Config) (*Client, CloseFunc, error) {
config.SetsDefault()
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(config.Region))
if err != nil {
return nil, err
return nil, nil, err
}
s3Client := s3.NewFromConfig(cfg)
uploadClient := s3manager.NewUploader(s3Client)
cdnClient := cloudfront.NewFromConfig(cfg)
return &Client{
client := &Client{
cdnClient: cdnClient,
s3Client: s3Client,
uploadClient: uploadClient,
distributionID: config.DistributionID,
cacheInvalidationStrategy: config.CacheInvalidationStrategy,
cacheInvalidationWaitTimeout: config.CacheInvalidationWaitTimeout,
BucketID: config.Bucket,
}, nil
bucketID: config.Bucket,
}
clientClose := func(ctx context.Context) error {
// ensure that all keys are invalidated
return client.Flush(ctx)
}
return client, clientClose, nil
}
// Close closes the client.
// Flush flushes the client by invalidating the CDN cache for modified keys.
// It waits for all invalidations to finish.
// It returns nil on success or an error.
// The error will be of type InvalidationError if the CDN cache could not be invalidated.
func (c *Client) Close(ctx context.Context) error {
func (c *Client) Flush(ctx context.Context) error {
c.mux.Lock()
defer c.mux.Unlock()
@ -138,7 +144,7 @@ func (c *Client) Close(ctx context.Context) error {
// invalidate invalidates the CDN cache for the given keys.
// It either performs the invalidation immediately or adds them to the list of dirty keys.
func (c *Client) invalidate(ctx context.Context, keys []string) error {
if c.cacheInvalidationStrategy == CacheInvalidateBatchOnClose {
if c.cacheInvalidationStrategy == CacheInvalidateBatchOnFlush {
// save as dirty key for batch invalidation on Close
c.mux.Lock()
defer c.mux.Unlock()
@ -218,7 +224,15 @@ type uploadClient interface {
}
type getClient interface {
GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
GetObject(
ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options),
) (*s3.GetObjectOutput, error)
}
type listClient interface {
ListObjectsV2(
ctx context.Context, params *s3.ListObjectsV2Input, optFns ...func(*s3.Options),
) (*s3.ListObjectsV2Output, error)
}
type deleteClient interface {
@ -241,8 +255,9 @@ type cdnClient interface {
}
type objectStorageClient interface {
deleteClient
getClient
listClient
deleteClient
}
// statically assert that Client implements the uploadClient interface.
@ -254,3 +269,6 @@ var _ objectStorageClient = (*Client)(nil)
func ptr[T any](t T) *T {
return &t
}
// CloseFunc is a function that closes the client.
type CloseFunc func(ctx context.Context) error

View File

@ -59,7 +59,7 @@ func TestUpload(t *testing.T) {
},
"lazy invalidation": {
in: newInput(),
cacheInvalidationStrategy: CacheInvalidateBatchOnClose,
cacheInvalidationStrategy: CacheInvalidateBatchOnFlush,
cacheInvalidationWaitTimeout: time.Microsecond,
wantDirtyKeys: []string{"test-key"},
},
@ -181,7 +181,7 @@ func TestDeleteObject(t *testing.T) {
wantInvalidationIDs: []string{"test-invalidation-id-1"},
},
"lazy invalidation": {
cacheInvalidationStrategy: CacheInvalidateBatchOnClose,
cacheInvalidationStrategy: CacheInvalidateBatchOnFlush,
cacheInvalidationWaitTimeout: time.Microsecond,
wantDirtyKeys: []string{"test-key"},
},
@ -273,7 +273,7 @@ func TestDeleteObject(t *testing.T) {
}
}
func TestClose(t *testing.T) {
func TestFlush(t *testing.T) {
testCases := map[string]struct {
dirtyKeys []string
invalidationIDs []string
@ -389,7 +389,7 @@ func TestClose(t *testing.T) {
dirtyKeys: tc.dirtyKeys,
invalidationIDs: tc.invalidationIDs,
}
err := client.Close(context.Background())
err := client.Flush(context.Background())
if tc.wantCacheInvalidationErr {
assert.ErrorAs(err, &InvalidationError{})
@ -450,9 +450,9 @@ func TestConcurrency(_ *testing.T) {
},
})
}
closeClient := func() {
flushClient := func() {
defer wg.Done()
_ = client.Close(context.Background())
_ = client.Flush(context.Background())
}
for i := 0; i < 100; i++ {
@ -460,7 +460,7 @@ func TestConcurrency(_ *testing.T) {
go upload()
go deleteObject()
go deleteObjects()
go closeClient()
go flushClient()
}
wg.Wait()
@ -559,3 +559,11 @@ func (s *stubObjectStorageClient) GetObject(
) (*s3.GetObjectOutput, error) {
return nil, nil
}
// currently not needed so no-Op.
func (s *stubObjectStorageClient) ListObjectsV2(
_ context.Context, _ *s3.ListObjectsV2Input,
_ ...func(*s3.Options),
) (*s3.ListObjectsV2Output, error) {
return nil, nil
}