image: add go code to upload image info and measurements

This commit is contained in:
Malte Poll 2023-05-23 15:09:14 +02:00 committed by Malte Poll
parent b8751f35f9
commit 217a744606
14 changed files with 745 additions and 3 deletions

View File

@ -9,6 +9,11 @@ go_library(
"flags.go", "flags.go",
"gcp.go", "gcp.go",
"image.go", "image.go",
"info.go",
"measurements.go",
"measurementsenvelope.go",
"measurementsmerge.go",
"measurementsupload.go",
"must.go", "must.go",
"nop.go", "nop.go",
"openstack.go", "openstack.go",
@ -19,6 +24,7 @@ go_library(
importpath = "github.com/edgelesssys/constellation/v2/image/upload/internal/cmd", importpath = "github.com/edgelesssys/constellation/v2/image/upload/internal/cmd",
visibility = ["//image/upload:__subpackages__"], visibility = ["//image/upload:__subpackages__"],
deps = [ deps = [
"//internal/attestation/measurements",
"//internal/cloud/cloudprovider", "//internal/cloud/cloudprovider",
"//internal/logger", "//internal/logger",
"//internal/osimage", "//internal/osimage",
@ -26,6 +32,8 @@ go_library(
"//internal/osimage/aws", "//internal/osimage/aws",
"//internal/osimage/azure", "//internal/osimage/azure",
"//internal/osimage/gcp", "//internal/osimage/gcp",
"//internal/osimage/imageinfo",
"//internal/osimage/measurementsuploader",
"//internal/osimage/nop", "//internal/osimage/nop",
"//internal/osimage/secureboot", "//internal/osimage/secureboot",
"//internal/versionsapi", "//internal/versionsapi",

View File

@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
package cmd package cmd
import ( import (
"errors"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@ -198,3 +199,143 @@ func parseGCPFlags(cmd *cobra.Command) (gcpFlags, error) {
gcpBucket: gcpBucket, gcpBucket: gcpBucket,
}, nil }, nil
} }
type s3Flags struct {
region string
bucket string
logLevel zapcore.Level
}
func parseS3Flags(cmd *cobra.Command) (s3Flags, error) {
region, err := cmd.Flags().GetString("region")
if err != nil {
return s3Flags{}, err
}
bucket, err := cmd.Flags().GetString("bucket")
if err != nil {
return s3Flags{}, err
}
verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return s3Flags{}, err
}
logLevel := zapcore.InfoLevel
if verbose {
logLevel = zapcore.DebugLevel
}
return s3Flags{
region: region,
bucket: bucket,
logLevel: logLevel,
}, nil
}
type measurementsFlags struct {
s3Flags
measurementsPath string
signaturePath string
}
func parseUploadMeasurementsFlags(cmd *cobra.Command) (measurementsFlags, error) {
s3, err := parseS3Flags(cmd)
if err != nil {
return measurementsFlags{}, err
}
measurementsPath, err := cmd.Flags().GetString("measurements")
if err != nil {
return measurementsFlags{}, err
}
signaturePath, err := cmd.Flags().GetString("signature")
if err != nil {
return measurementsFlags{}, err
}
return measurementsFlags{
s3Flags: s3,
measurementsPath: measurementsPath,
signaturePath: signaturePath,
}, nil
}
type mergeMeasurementsFlags struct {
out string
logLevel zapcore.Level
}
func parseMergeMeasurementsFlags(cmd *cobra.Command) (mergeMeasurementsFlags, error) {
out, err := cmd.Flags().GetString("out")
if err != nil {
return mergeMeasurementsFlags{}, err
}
verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return mergeMeasurementsFlags{}, err
}
logLevel := zapcore.InfoLevel
if verbose {
logLevel = zapcore.DebugLevel
}
return mergeMeasurementsFlags{
out: out,
logLevel: logLevel,
}, nil
}
type envelopeMeasurementsFlags struct {
version versionsapi.Version
csp cloudprovider.Provider
attestationVariant string
in, out string
logLevel zapcore.Level
}
func parseEnvelopeMeasurementsFlags(cmd *cobra.Command) (envelopeMeasurementsFlags, error) {
version, err := cmd.Flags().GetString("version")
if err != nil {
return envelopeMeasurementsFlags{}, err
}
ver, err := versionsapi.NewVersionFromShortPath(version, versionsapi.VersionKindImage)
if err != nil {
return envelopeMeasurementsFlags{}, err
}
csp, err := cmd.Flags().GetString("csp")
if err != nil {
return envelopeMeasurementsFlags{}, err
}
provider := cloudprovider.FromString(csp)
attestationVariant, err := cmd.Flags().GetString("attestation-variant")
if err != nil {
return envelopeMeasurementsFlags{}, err
}
if provider == cloudprovider.Unknown {
return envelopeMeasurementsFlags{}, errors.New("unknown cloud provider")
}
in, err := cmd.Flags().GetString("in")
if err != nil {
return envelopeMeasurementsFlags{}, err
}
out, err := cmd.Flags().GetString("out")
if err != nil {
return envelopeMeasurementsFlags{}, err
}
verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return envelopeMeasurementsFlags{}, err
}
logLevel := zapcore.InfoLevel
if verbose {
logLevel = zapcore.DebugLevel
}
return envelopeMeasurementsFlags{
version: ver,
csp: provider,
attestationVariant: attestationVariant,
in: in,
out: out,
logLevel: logLevel,
}, nil
}

View File

@ -0,0 +1,83 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"encoding/json"
"fmt"
"os"
"github.com/edgelesssys/constellation/v2/internal/logger"
infoupload "github.com/edgelesssys/constellation/v2/internal/osimage/imageinfo"
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
"github.com/spf13/cobra"
)
// NewInfoCmd creates a new info parent command.
func NewInfoCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "info [flags] <image-info.json>...",
Short: "Uploads OS image info to S3",
Long: "Uploads OS image info to S3.",
Args: cobra.MinimumNArgs(1),
RunE: runInfo,
}
cmd.SetOut(os.Stdout)
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().Bool("verbose", false, "Enable verbose output")
return cmd
}
func runInfo(cmd *cobra.Command, args []string) error {
workdir := os.Getenv("BUILD_WORKING_DIRECTORY")
if len(workdir) > 0 {
must(os.Chdir(workdir))
}
flags, err := parseS3Flags(cmd)
if err != nil {
return err
}
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
info, err := readInfoArgs(args)
if err != nil {
return err
}
uploadC, err := infoupload.New(cmd.Context(), flags.region, flags.bucket, log)
if err != nil {
return fmt.Errorf("uploading image info: %w", err)
}
url, err := uploadC.Upload(cmd.Context(), info)
if err != nil {
return fmt.Errorf("uploading image info: %w", err)
}
log.Infof("Uploaded image info to %s", url)
return nil
}
func readInfoArgs(paths []string) (versionsapi.ImageInfo, error) {
infos := make([]versionsapi.ImageInfo, len(paths))
for i, path := range paths {
f, err := os.Open(path)
if err != nil {
return versionsapi.ImageInfo{}, err
}
defer f.Close()
if err := json.NewDecoder(f).Decode(&infos[i]); err != nil {
return versionsapi.ImageInfo{}, err
}
}
return versionsapi.MergeImageInfos(infos...)
}

View File

@ -0,0 +1,32 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"os"
"github.com/spf13/cobra"
)
// NewMeasurementsCmd creates a new measurements command. Measurements needs another
// verb, and does nothing on its own.
func NewMeasurementsCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "measurements",
Short: "Handle OS image measurements",
Long: "Handle OS image measurements.",
Args: cobra.ExactArgs(0),
}
cmd.SetOut(os.Stdout)
cmd.AddCommand(newMeasurementsUploadCmd())
cmd.AddCommand(newMeasurementsMergeCmd())
cmd.AddCommand(newMeasurementsEnvelopeCmd())
return cmd
}

View File

@ -0,0 +1,101 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"encoding/json"
"fmt"
"os"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/spf13/cobra"
)
// newMeasurementsEnvelopeCmd creates a new envelope command.
func newMeasurementsEnvelopeCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "envelope",
Short: "Envelope OS image measurements",
Long: "Envelope OS image measurements for one variant to follow the measurements v2 format.",
Args: cobra.ExactArgs(0),
RunE: runEnvelopeMeasurements,
}
cmd.SetOut(os.Stdout)
cmd.Flags().String("version", "", "Shortname of the os image version.")
cmd.Flags().String("csp", "", "CSP of this image measurement.")
cmd.Flags().String("attestation-variant", "", "Attestation variant of the image measurements.")
cmd.Flags().String("in", "", "Path to read the raw measurements from.")
cmd.Flags().String("out", "", "Optional path to write the enveloped result to. If not set, the result is written to stdout.")
cmd.Flags().Bool("verbose", false, "Enable verbose output")
must(cmd.MarkFlagRequired("version"))
must(cmd.MarkFlagRequired("csp"))
must(cmd.MarkFlagRequired("attestation-variant"))
must(cmd.MarkFlagRequired("in"))
return cmd
}
func runEnvelopeMeasurements(cmd *cobra.Command, _ []string) error {
workdir := os.Getenv("BUILD_WORKING_DIRECTORY")
if len(workdir) > 0 {
must(os.Chdir(workdir))
}
flags, err := parseEnvelopeMeasurementsFlags(cmd)
if err != nil {
return err
}
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
f, err := os.Open(flags.in)
if err != nil {
return fmt.Errorf("enveloping measurements: opening input file: %w", err)
}
defer f.Close()
var measuremnt rawMeasurements
if err := json.NewDecoder(f).Decode(&measuremnt); err != nil {
return fmt.Errorf("enveloping measurements: reading input file: %w", err)
}
enveloped := measurements.ImageMeasurementsV2{
Ref: flags.version.Ref,
Stream: flags.version.Stream,
Version: flags.version.Version,
List: []measurements.ImageMeasurementsV2Entry{
{
CSP: flags.csp,
AttestationVariant: flags.attestationVariant,
Measurements: measuremnt.Measurements,
},
},
}
out := cmd.OutOrStdout()
if len(flags.out) > 0 {
outF, err := os.Create(flags.out)
if err != nil {
return fmt.Errorf("enveloping measurements: opening output file: %w", err)
}
defer outF.Close()
out = outF
}
if err := json.NewEncoder(out).Encode(enveloped); err != nil {
return fmt.Errorf("enveloping measurements: writing output file: %w", err)
}
log.Infof("Enveloped image measurements")
return nil
}
type rawMeasurements struct {
Measurements measurements.M `json:"measurements"`
}

View File

@ -0,0 +1,85 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"encoding/json"
"fmt"
"os"
"github.com/edgelesssys/constellation/v2/internal/attestation/measurements"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/spf13/cobra"
)
// newMeasurementsMergeCmd creates a new merge command.
func newMeasurementsMergeCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "merge [flags] <measurements.json>...",
Short: "Merge OS image measurements",
Long: "Merge OS image measurements.",
Args: cobra.MinimumNArgs(1),
RunE: runMergeMeasurements,
}
cmd.SetOut(os.Stdout)
cmd.Flags().String("out", "", "Optional path to write the merge result to. If not set, the result is written to stdout.")
cmd.Flags().Bool("verbose", false, "Enable verbose output")
return cmd
}
func runMergeMeasurements(cmd *cobra.Command, args []string) error {
workdir := os.Getenv("BUILD_WORKING_DIRECTORY")
if len(workdir) > 0 {
must(os.Chdir(workdir))
}
flags, err := parseMergeMeasurementsFlags(cmd)
if err != nil {
return err
}
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
mergedMeasurements, err := readMeasurementsArgs(args)
if err != nil {
return fmt.Errorf("merging measurements: reading input files: %w", err)
}
out := cmd.OutOrStdout()
if len(flags.out) > 0 {
outF, err := os.Create(flags.out)
if err != nil {
return fmt.Errorf("merging measurements: opening output file: %w", err)
}
defer outF.Close()
out = outF
}
if err := json.NewEncoder(out).Encode(mergedMeasurements); err != nil {
return fmt.Errorf("merging measurements: writing output file: %w", err)
}
log.Infof("Merged image measurements")
return nil
}
func readMeasurementsArgs(paths []string) (measurements.ImageMeasurementsV2, error) {
measuremnts := make([]measurements.ImageMeasurementsV2, len(paths))
for i, path := range paths {
f, err := os.Open(path)
if err != nil {
return measurements.ImageMeasurementsV2{}, err
}
defer f.Close()
if err := json.NewDecoder(f).Decode(&measuremnts[i]); err != nil {
return measurements.ImageMeasurementsV2{}, err
}
}
return measurements.MergeImageMeasurementsV2(measuremnts...)
}

View File

@ -0,0 +1,78 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"fmt"
"os"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/osimage/measurementsuploader"
"github.com/spf13/cobra"
)
// newMeasurementsUploadCmd creates a new upload command.
func newMeasurementsUploadCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "upload",
Short: "Uploads OS image measurements to S3",
Long: "Uploads OS image measurements to S3.",
Args: cobra.ExactArgs(0),
RunE: runMeasurementsUpload,
}
cmd.SetOut(os.Stdout)
cmd.Flags().String("measurements", "", "Path to measurements file to upload")
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().Bool("verbose", false, "Enable verbose output")
must(cmd.MarkFlagRequired("measurements"))
must(cmd.MarkFlagRequired("signature"))
return cmd
}
func runMeasurementsUpload(cmd *cobra.Command, _ []string) error {
workdir := os.Getenv("BUILD_WORKING_DIRECTORY")
if len(workdir) > 0 {
must(os.Chdir(workdir))
}
flags, err := parseUploadMeasurementsFlags(cmd)
if err != nil {
return err
}
log := logger.New(logger.PlainLog, flags.logLevel)
log.Debugf("Parsed flags: %+v", flags)
uploadC, err := measurementsuploader.New(cmd.Context(), flags.region, flags.bucket, log)
if err != nil {
return fmt.Errorf("uploading image info: %w", err)
}
measurements, err := os.Open(flags.measurementsPath)
if err != nil {
return fmt.Errorf("uploading image measurements: opening measurements file: %w", err)
}
defer measurements.Close()
signature, err := os.Open(flags.signaturePath)
if err != nil {
return fmt.Errorf("uploading image measurements: opening signature file: %w", err)
}
defer signature.Close()
measurementsURL, signatureURL, err := uploadC.Upload(cmd.Context(), measurements, signature)
if err != nil {
return fmt.Errorf("uploading image info: %w", err)
}
log.Infof("Uploaded image measurements to %s (and signature to %s)", measurementsURL, signatureURL)
return nil
}

View File

@ -41,6 +41,8 @@ func newRootCmd() *cobra.Command {
rootCmd.SetOut(os.Stdout) rootCmd.SetOut(os.Stdout)
rootCmd.AddCommand(cmd.NewImageCmd()) rootCmd.AddCommand(cmd.NewImageCmd())
rootCmd.AddCommand(cmd.NewInfoCmd())
rootCmd.AddCommand(cmd.NewMeasurementsCmd())
return rootCmd return rootCmd
} }

View File

@ -6,6 +6,7 @@ go_library(
importpath = "github.com/edgelesssys/constellation/v2/internal/osimage/archive", importpath = "github.com/edgelesssys/constellation/v2/internal/osimage/archive",
visibility = ["//:__subpackages__"], visibility = ["//:__subpackages__"],
deps = [ deps = [
"//internal/constants",
"//internal/logger", "//internal/logger",
"//internal/versionsapi", "//internal/versionsapi",
"@com_github_aws_aws_sdk_go_v2_config//:config", "@com_github_aws_aws_sdk_go_v2_config//:config",

View File

@ -16,6 +16,7 @@ import (
s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager" s3manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" 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/logger"
"github.com/edgelesssys/constellation/v2/internal/versionsapi" "github.com/edgelesssys/constellation/v2/internal/versionsapi"
) )
@ -58,11 +59,9 @@ func (a *Archivist) Archive(ctx context.Context, version versionsapi.Version, cs
Body: img, Body: img,
ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256, ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256,
}) })
return baseURL + key, err return constants.CDNRepositoryURL + "/" + key, err
} }
type uploadClient interface { type uploadClient interface {
Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error) Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error)
} }
const baseURL = "https://cdn.confidential.cloud/"

View File

@ -0,0 +1,17 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "imageinfo",
srcs = ["imageinfo.go"],
importpath = "github.com/edgelesssys/constellation/v2/internal/osimage/imageinfo",
visibility = ["//:__subpackages__"],
deps = [
"//internal/constants",
"//internal/logger",
"//internal/versionsapi",
"@com_github_aws_aws_sdk_go_v2_config//:config",
"@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

@ -0,0 +1,78 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
// package imageinfo is used to upload image info JSON files to S3.
package imageinfo
import (
"bytes"
"context"
"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"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/versionsapi"
)
// Uploader uploads image info to S3.
type Uploader struct {
uploadClient uploadClient
// bucket is the name of the S3 bucket to use.
bucket string
log *logger.Logger
}
// 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))
if err != nil {
return nil, err
}
s3client := s3.NewFromConfig(cfg)
uploadClient := s3manager.NewUploader(s3client)
return &Uploader{
uploadClient: uploadClient,
bucket: bucket,
log: log,
}, nil
}
// Upload marshals the image info to JSON and uploads it to S3.
func (a *Uploader) Upload(ctx context.Context, imageInfo versionsapi.ImageInfo) (string, error) {
ver := versionsapi.Version{
Ref: imageInfo.Ref,
Stream: imageInfo.Stream,
Version: imageInfo.Version,
Kind: versionsapi.VersionKindImage,
}
key, err := url.JoinPath(ver.ArtifactPath(versionsapi.APIV2), ver.Kind.String(), "info.json")
if err != nil {
return "", err
}
a.log.Debugf("Archiving image info to s3://%v/%v", a.bucket, key)
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(imageInfo); err != nil {
return "", err
}
_, err = a.uploadClient.Upload(ctx, &s3.PutObjectInput{
Bucket: &a.bucket,
Key: &key,
Body: buf,
ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256,
})
return constants.CDNRepositoryURL + "/" + key, err
}
type uploadClient interface {
Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error)
}

View File

@ -0,0 +1,18 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "measurementsuploader",
srcs = ["measurementsuploader.go"],
importpath = "github.com/edgelesssys/constellation/v2/internal/osimage/measurementsuploader",
visibility = ["//:__subpackages__"],
deps = [
"//internal/attestation/measurements",
"//internal/constants",
"//internal/logger",
"//internal/versionsapi",
"@com_github_aws_aws_sdk_go_v2_config//:config",
"@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

@ -0,0 +1,99 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
// package measurementsuploader is used to upload measurements (v2) JSON files (and signatures) to S3.
package measurementsuploader
import (
"context"
"encoding/json"
"fmt"
"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"
"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/versionsapi"
)
// Uploader uploads image info to S3.
type Uploader struct {
uploadClient uploadClient
// bucket is the name of the S3 bucket to use.
bucket string
log *logger.Logger
}
// 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))
if err != nil {
return nil, err
}
s3client := s3.NewFromConfig(cfg)
uploadClient := s3manager.NewUploader(s3client)
return &Uploader{
uploadClient: uploadClient,
bucket: bucket,
log: log,
}, nil
}
// Upload uploads the measurements v2 JSON file and its signature to S3.
func (a *Uploader) Upload(ctx context.Context, rawMeasurement, signature io.ReadSeeker) (string, string, error) {
// parse the measurements to get the ref, stream, and version
var measurements measurements.ImageMeasurementsV2
if err := json.NewDecoder(rawMeasurement).Decode(&measurements); err != nil {
return "", "", err
}
if _, err := rawMeasurement.Seek(0, io.SeekStart); err != nil {
return "", "", err
}
ver := versionsapi.Version{
Ref: measurements.Ref,
Stream: measurements.Stream,
Version: measurements.Version,
Kind: versionsapi.VersionKindImage,
}
key, err := url.JoinPath(ver.ArtifactPath(versionsapi.APIV2), ver.Kind.String(), "measurements.json")
if err != nil {
return "", "", err
}
sigKey, err := url.JoinPath(ver.ArtifactPath(versionsapi.APIV2), ver.Kind.String(), "measurements.json.sig")
if err != nil {
return "", "", err
}
a.log.Debugf("Archiving image measurements to s3://%v/%v and s3://%v/%v", a.bucket, key, a.bucket, sigKey)
if _, err = a.uploadClient.Upload(ctx, &s3.PutObjectInput{
Bucket: &a.bucket,
Key: &key,
Body: rawMeasurement,
ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256,
}); err != nil {
return "", "", fmt.Errorf("uploading measurements: %w", err)
}
if _, err = a.uploadClient.Upload(ctx, &s3.PutObjectInput{
Bucket: &a.bucket,
Key: &sigKey,
Body: signature,
ChecksumAlgorithm: s3types.ChecksumAlgorithmSha256,
}); err != nil {
return "", "", fmt.Errorf("uploading measurements signature: %w", err)
}
return constants.CDNRepositoryURL + "/" + key, constants.CDNRepositoryURL + "/" + sigKey, nil
}
type uploadClient interface {
Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error)
}