config: Azure SNP tool can delete specific version from attestation API (#1863)

* client supports delete version

* rename to new attestation / fetcher naming

* add delete command to upload tool

* test client delete

* bazel update

* use general client in attestation client

* Update hack/configapi/cmd/delete.go

Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com>

* daniel feedback

* unit test azure sev upload

* Update hack/configapi/cmd/delete.go

Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com>

* add client integration test

* new client cmds use apiObject

---------

Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com>
This commit is contained in:
Adrian Stobbe 2023-06-05 12:33:22 +02:00 committed by GitHub
parent 315b6c2f01
commit c446f36b0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 549 additions and 228 deletions

View File

@ -16,7 +16,7 @@ import (
"net/url" "net/url"
"testing" "testing"
configapi "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
versionsapi "github.com/edgelesssys/constellation/v2/internal/api/versions" versionsapi "github.com/edgelesssys/constellation/v2/internal/api/versions"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/config"
@ -281,7 +281,7 @@ func TestConfigFetchMeasurements(t *testing.T) {
require.NoError(err) require.NoError(err)
cfm := &configFetchMeasurementsCmd{canFetchMeasurements: true, log: logger.NewTest(t)} cfm := &configFetchMeasurementsCmd{canFetchMeasurements: true, log: logger.NewTest(t)}
err = cfm.configFetchMeasurements(cmd, tc.cosign, tc.rekor, fileHandler, fakeConfigFetcher{}, client) err = cfm.configFetchMeasurements(cmd, tc.cosign, tc.rekor, fileHandler, fakeAttestationFetcher{}, client)
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)
return return
@ -291,27 +291,27 @@ func TestConfigFetchMeasurements(t *testing.T) {
} }
} }
type fakeConfigFetcher struct{} type fakeAttestationFetcher struct{}
func (f fakeConfigFetcher) FetchAzureSEVSNPVersionList(_ context.Context, _ configapi.AzureSEVSNPVersionList) (configapi.AzureSEVSNPVersionList, error) { func (f fakeAttestationFetcher) FetchAzureSEVSNPVersionList(_ context.Context, _ attestationconfig.AzureSEVSNPVersionList) (attestationconfig.AzureSEVSNPVersionList, error) {
return configapi.AzureSEVSNPVersionList( return attestationconfig.AzureSEVSNPVersionList(
[]string{}, []string{},
), nil ), nil
} }
func (f fakeConfigFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ configapi.AzureSEVSNPVersionGet) (configapi.AzureSEVSNPVersionGet, error) { func (f fakeAttestationFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ attestationconfig.AzureSEVSNPVersionAPI) (attestationconfig.AzureSEVSNPVersionAPI, error) {
return configapi.AzureSEVSNPVersionGet{ return attestationconfig.AzureSEVSNPVersionAPI{
AzureSEVSNPVersion: testCfg, AzureSEVSNPVersion: testCfg,
}, nil }, nil
} }
func (f fakeConfigFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (configapi.AzureSEVSNPVersionGet, error) { func (f fakeAttestationFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (attestationconfig.AzureSEVSNPVersionAPI, error) {
return configapi.AzureSEVSNPVersionGet{ return attestationconfig.AzureSEVSNPVersionAPI{
AzureSEVSNPVersion: testCfg, AzureSEVSNPVersion: testCfg,
}, nil }, nil
} }
var testCfg = configapi.AzureSEVSNPVersion{ var testCfg = attestationconfig.AzureSEVSNPVersion{
Microcode: 93, Microcode: 93,
TEE: 0, TEE: 0,
SNP: 6, SNP: 6,

View File

@ -202,7 +202,7 @@ func TestCreate(t *testing.T) {
fileHandler := file.NewHandler(tc.setupFs(require, tc.provider)) fileHandler := file.NewHandler(tc.setupFs(require, tc.provider))
c := &createCmd{log: logger.NewTest(t)} c := &createCmd{log: logger.NewTest(t)}
err := c.create(cmd, tc.creator, fileHandler, &nopSpinner{}, fakeConfigFetcher{}) err := c.create(cmd, tc.creator, fileHandler, &nopSpinner{}, fakeAttestationFetcher{})
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)

View File

@ -175,7 +175,7 @@ func TestInitialize(t *testing.T) {
defer cancel() defer cancel()
cmd.SetContext(ctx) cmd.SetContext(ctx)
i := &initCmd{log: logger.NewTest(t), spinner: &nopSpinner{}} i := &initCmd{log: logger.NewTest(t), spinner: &nopSpinner{}}
err := i.initialize(cmd, newDialer, fileHandler, &stubLicenseClient{}, fakeConfigFetcher{}) err := i.initialize(cmd, newDialer, fileHandler, &stubLicenseClient{}, fakeAttestationFetcher{})
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)
@ -519,7 +519,7 @@ func TestAttestation(t *testing.T) {
cmd.SetContext(ctx) cmd.SetContext(ctx)
i := &initCmd{log: logger.NewTest(t), spinner: &nopSpinner{}} i := &initCmd{log: logger.NewTest(t), spinner: &nopSpinner{}}
err := i.initialize(cmd, newDialer, fileHandler, &stubLicenseClient{}, fakeConfigFetcher{}) err := i.initialize(cmd, newDialer, fileHandler, &stubLicenseClient{}, fakeAttestationFetcher{})
assert.Error(err) assert.Error(err)
// make sure the error is actually a TLS handshake error // make sure the error is actually a TLS handshake error
assert.Contains(err.Error(), "transport: authentication handshake failed") assert.Contains(err.Error(), "transport: authentication handshake failed")

View File

@ -163,7 +163,7 @@ func TestRecover(t *testing.T) {
)) ))
newDialer := func(atls.Validator) *dialer.Dialer { return nil } newDialer := func(atls.Validator) *dialer.Dialer { return nil }
r := &recoverCmd{log: logger.NewTest(t), configFetcher: fakeConfigFetcher{}} r := &recoverCmd{log: logger.NewTest(t), configFetcher: fakeAttestationFetcher{}}
err := r.recover(cmd, fileHandler, time.Millisecond, tc.doer, newDialer) err := r.recover(cmd, fileHandler, time.Millisecond, tc.doer, newDialer)
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)

View File

@ -142,7 +142,7 @@ func TestUpgradeApply(t *testing.T) {
require.NoError(handler.WriteYAML(constants.ConfigFilename, cfg)) require.NoError(handler.WriteYAML(constants.ConfigFilename, cfg))
require.NoError(handler.WriteJSON(constants.ClusterIDsFileName, clusterid.File{})) require.NoError(handler.WriteJSON(constants.ClusterIDsFileName, clusterid.File{}))
upgrader := upgradeApplyCmd{upgrader: tc.upgrader, log: logger.NewTest(t), imageFetcher: tc.fetcher, configFetcher: fakeConfigFetcher{}} upgrader := upgradeApplyCmd{upgrader: tc.upgrader, log: logger.NewTest(t), imageFetcher: tc.fetcher, configFetcher: fakeAttestationFetcher{}}
err := upgrader.upgradeApply(cmd, handler) err := upgrader.upgradeApply(cmd, handler)
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)

View File

@ -271,7 +271,7 @@ func TestUpgradeCheck(t *testing.T) {
cmd := newUpgradeCheckCmd() cmd := newUpgradeCheckCmd()
err := checkCmd.upgradeCheck(cmd, fileHandler, fakeConfigFetcher{}, tc.flags) err := checkCmd.upgradeCheck(cmd, fileHandler, fakeAttestationFetcher{}, tc.flags)
if tc.wantError { if tc.wantError {
assert.Error(err) assert.Error(err)
return return

View File

@ -190,7 +190,7 @@ func TestVerify(t *testing.T) {
} }
v := &verifyCmd{log: logger.NewTest(t)} v := &verifyCmd{log: logger.NewTest(t)}
err := v.verify(cmd, fileHandler, tc.protoClient, tc.formatter, fakeConfigFetcher{}) err := v.verify(cmd, fileHandler, tc.protoClient, tc.formatter, fakeAttestationFetcher{})
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)

View File

@ -3,24 +3,33 @@ load("//bazel/go:go_test.bzl", "go_test")
go_library( go_library(
name = "cmd", name = "cmd",
srcs = ["root.go"], srcs = [
"delete.go",
"root.go",
],
importpath = "github.com/edgelesssys/constellation/v2/hack/configapi/cmd", importpath = "github.com/edgelesssys/constellation/v2/hack/configapi/cmd",
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
"//internal/api/attestationconfig", "//internal/api/attestationconfig",
"//internal/api/attestationconfig/client", "//internal/api/attestationconfig/client",
"//internal/api/attestationconfig/fetcher", "//internal/api/attestationconfig/fetcher",
"//internal/logger",
"//internal/staticupload", "//internal/staticupload",
"@com_github_spf13_cobra//:cobra", "@com_github_spf13_cobra//:cobra",
"@org_uber_go_zap//:zap",
], ],
) )
go_test( go_test(
name = "cmd_test", name = "cmd_test",
srcs = ["root_test.go"], srcs = [
"delete_test.go",
"root_test.go",
],
embed = [":cmd"], embed = [":cmd"],
deps = [ deps = [
"//internal/api/attestationconfig", "//internal/api/attestationconfig",
"@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
], ],
) )

View File

@ -0,0 +1,63 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"context"
"fmt"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/client"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
"github.com/spf13/cobra"
)
// newDeleteCmd creates the delete command.
func newDeleteCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "delete",
Short: "delete a specific version from the config api",
RunE: runDelete,
}
cmd.Flags().StringP("version", "v", "", "Name of the version to delete (without .json suffix)")
must(enforceRequiredFlags(cmd, "version"))
return cmd
}
type deleteCmd struct {
attestationClient deleteClient
}
type deleteClient interface {
DeleteAzureSEVSNPVersion(ctx context.Context, versionStr string) error
}
func (d deleteCmd) delete(cmd *cobra.Command) error {
version, err := cmd.Flags().GetString("version")
if err != nil {
return err
}
return d.attestationClient.DeleteAzureSEVSNPVersion(cmd.Context(), version)
}
func runDelete(cmd *cobra.Command, _ []string) error {
cfg := staticupload.Config{
Bucket: awsBucket,
Region: awsRegion,
}
repo, closefn, err := client.New(cmd.Context(), cfg, []byte(cosignPwd), []byte(privateKey), false, log())
if err != nil {
return fmt.Errorf("create attestation client: %w", err)
}
defer func() {
if err := closefn(cmd.Context()); err != nil {
cmd.Printf("close client: %s\n", err.Error())
}
}()
deleteCmd := deleteCmd{
attestationClient: repo,
}
return deleteCmd.delete(cmd)
}

View File

@ -0,0 +1,38 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDeleteVersion(t *testing.T) {
client := &fakeAttestationClient{}
sut := deleteCmd{
attestationClient: client,
}
cmd := newDeleteCmd()
require.NoError(t, cmd.Flags().Set("version", "2021-01-01"))
assert.NoError(t, sut.delete(cmd))
assert.True(t, client.isCalled)
}
type fakeAttestationClient struct {
isCalled bool
}
func (f *fakeAttestationClient) DeleteAzureSEVSNPVersion(_ context.Context, version string) error {
if version == "2021-01-01" {
f.isCalled = true
return nil
}
return errors.New("version does not exist")
}

View File

@ -16,6 +16,8 @@ import (
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
attestationconfigclient "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/client" attestationconfigclient "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/client"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/fetcher" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/fetcher"
"github.com/edgelesssys/constellation/v2/internal/logger"
"go.uber.org/zap"
"github.com/edgelesssys/constellation/v2/internal/staticupload" "github.com/edgelesssys/constellation/v2/internal/staticupload"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -54,8 +56,8 @@ func newRootCmd() *cobra.Command {
RunE: runCmd, RunE: runCmd,
} }
rootCmd.PersistentFlags().StringVarP(&versionFilePath, "version-file", "f", "", "File path to the version json file.") rootCmd.PersistentFlags().StringVarP(&versionFilePath, "version-file", "f", "", "File path to the version json file.")
must(enforceRequiredFlags(rootCmd, "version-file")) must(enforcePersistentRequiredFlags(rootCmd, "version-file"))
rootCmd.AddCommand(newDeleteCmd())
return rootCmd return rootCmd
} }
@ -93,11 +95,11 @@ func runCmd(cmd *cobra.Command, _ []string) error {
return fmt.Errorf("comparing versions: %w", err) return fmt.Errorf("comparing versions: %w", err)
} }
if isNewer { if isNewer {
fmt.Printf("Input version: %+v is newer than latest API version: %+v\n", inputVersion, latestAPIVersion) cmd.Printf("Input version: %+v is newer than latest API version: %+v\n", inputVersion, latestAPIVersion)
sut, sutClose, err := attestationconfigclient.New(ctx, cfg, []byte(cosignPwd), []byte(privateKey)) sut, sutClose, err := attestationconfigclient.New(ctx, cfg, []byte(cosignPwd), []byte(privateKey), false, log())
defer func() { defer func() {
if err := sutClose(ctx); err != nil { if err := sutClose(ctx); err != nil {
fmt.Printf("closing repo: %v\n", err) cmd.Printf("closing repo: %v\n", err)
} }
}() }()
if err != nil { if err != nil {
@ -143,6 +145,15 @@ func isInputNewerThanLatestAPI(input, latest attestationconfig.AzureSEVSNPVersio
} }
func enforceRequiredFlags(cmd *cobra.Command, flags ...string) error { func enforceRequiredFlags(cmd *cobra.Command, flags ...string) error {
for _, flag := range flags {
if err := cmd.MarkFlagRequired(flag); err != nil {
return err
}
}
return nil
}
func enforcePersistentRequiredFlags(cmd *cobra.Command, flags ...string) error {
for _, flag := range flags { for _, flag := range flags {
if err := cmd.MarkPersistentFlagRequired(flag); err != nil { if err := cmd.MarkPersistentFlagRequired(flag); err != nil {
return err return err
@ -156,3 +167,7 @@ func must(err error) {
panic(err) panic(err)
} }
} }
func log() *logger.Logger {
return logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfig")
}

View File

@ -34,29 +34,53 @@ type AzureSEVSNPVersion struct {
Microcode uint8 `json:"microcode"` Microcode uint8 `json:"microcode"`
} }
// AzureSEVSNPVersionGet is the request to get the version information of the specific version in the config api. // AzureSEVSNPVersionSignature is the object to perform CRUD operations on the config api.
type AzureSEVSNPVersionGet struct { type AzureSEVSNPVersionSignature struct {
Version string `json:"-"`
Signature []byte `json:"signature"`
}
// JSONPath returns the path to the JSON file for the request to the config api.
func (s AzureSEVSNPVersionSignature) JSONPath() string {
return path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), s.Version, ".sig")
}
// URL returns the URL for the request to the config api.
func (s AzureSEVSNPVersionSignature) URL() (string, error) {
return getURL(s)
}
// ValidateRequest validates the request.
func (s AzureSEVSNPVersionSignature) ValidateRequest() error {
if !strings.HasSuffix(s.Version, ".json") {
return fmt.Errorf("version has no .json suffix")
}
return nil
}
// Validate is a No-Op at the moment.
func (s AzureSEVSNPVersionSignature) Validate() error {
return nil
}
// AzureSEVSNPVersionAPI is the request to get the version information of the specific version in the config api.
type AzureSEVSNPVersionAPI struct {
Version string `json:"-"` Version string `json:"-"`
AzureSEVSNPVersion AzureSEVSNPVersion
} }
// URL returns the URL for the request to the config api. // URL returns the URL for the request to the config api.
func (i AzureSEVSNPVersionGet) URL() (string, error) { func (i AzureSEVSNPVersionAPI) URL() (string, error) {
url, err := url.Parse(constants.CDNRepositoryURL) return getURL(i)
if err != nil {
return "", fmt.Errorf("parsing CDN URL: %w", err)
}
url.Path = i.JSONPath()
return url.String(), nil
} }
// JSONPath returns the path to the JSON file for the request to the config api. // JSONPath returns the path to the JSON file for the request to the config api.
func (i AzureSEVSNPVersionGet) JSONPath() string { func (i AzureSEVSNPVersionAPI) JSONPath() string {
return path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), i.Version) return path.Join(attestationURLPath, variant.AzureSEVSNP{}.String(), i.Version)
} }
// ValidateRequest validates the request. // ValidateRequest validates the request.
func (i AzureSEVSNPVersionGet) ValidateRequest() error { func (i AzureSEVSNPVersionAPI) ValidateRequest() error {
if !strings.HasSuffix(i.Version, ".json") { if !strings.HasSuffix(i.Version, ".json") {
return fmt.Errorf("version has no .json suffix") return fmt.Errorf("version has no .json suffix")
} }
@ -64,7 +88,7 @@ func (i AzureSEVSNPVersionGet) ValidateRequest() error {
} }
// Validate is a No-Op at the moment. // Validate is a No-Op at the moment.
func (i AzureSEVSNPVersionGet) Validate() error { func (i AzureSEVSNPVersionAPI) Validate() error {
return nil return nil
} }
@ -73,12 +97,7 @@ type AzureSEVSNPVersionList []string
// URL returns the URL for the request to the config api. // URL returns the URL for the request to the config api.
func (i AzureSEVSNPVersionList) URL() (string, error) { func (i AzureSEVSNPVersionList) URL() (string, error) {
url, err := url.Parse(constants.CDNRepositoryURL) return getURL(i)
if err != nil {
return "", fmt.Errorf("parsing CDN URL: %w", err)
}
url.Path = i.JSONPath()
return url.String(), nil
} }
// JSONPath returns the path to the JSON file for the request to the config api. // JSONPath returns the path to the JSON file for the request to the config api.
@ -98,3 +117,16 @@ func (i AzureSEVSNPVersionList) Validate() error {
} }
return nil return nil
} }
func getURL(obj jsoPather) (string, error) {
url, err := url.Parse(constants.CDNRepositoryURL)
if err != nil {
return "", fmt.Errorf("parsing CDN URL: %w", err)
}
url.Path = obj.JSONPath()
return url.String(), nil
}
type jsoPather interface {
JSONPath() string
}

View File

@ -8,13 +8,13 @@ go_library(
visibility = ["//:__subpackages__"], visibility = ["//:__subpackages__"],
deps = [ deps = [
"//internal/api/attestationconfig", "//internal/api/attestationconfig",
"//internal/api/attestationconfig/fetcher",
"//internal/api/client",
"//internal/constants", "//internal/constants",
"//internal/kms/storage", "//internal/logger",
"//internal/sigstore", "//internal/sigstore",
"//internal/staticupload", "//internal/staticupload",
"//internal/variant", "//internal/variant",
"@com_github_aws_aws_sdk_go_v2_feature_s3_manager//:manager",
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
], ],
) )
@ -23,14 +23,13 @@ go_test(
srcs = ["client_test.go"], srcs = ["client_test.go"],
# keep # keep
count = 1, count = 1,
embed = [":client"],
# keep # keep
gotags = ["e2e"], gotags = ["e2e"],
# keep # keep
tags = ["manual"], tags = ["manual"],
deps = [ deps = [
":client",
"//internal/api/attestationconfig", "//internal/api/attestationconfig",
"//internal/staticupload", "@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
], ],
) )

View File

@ -6,21 +6,17 @@ SPDX-License-Identifier: AGPL-3.0-only
package client package client
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"path"
"sort" "sort"
"time" "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/api/attestationconfig"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/fetcher"
apiclient "github.com/edgelesssys/constellation/v2/internal/api/client"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/kms/storage" "github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/sigstore" "github.com/edgelesssys/constellation/v2/internal/sigstore"
"github.com/edgelesssys/constellation/v2/internal/staticupload" "github.com/edgelesssys/constellation/v2/internal/staticupload"
"github.com/edgelesssys/constellation/v2/internal/variant" "github.com/edgelesssys/constellation/v2/internal/variant"
@ -28,70 +24,90 @@ import (
// Client manages (modifies) the version information for the attestation variants. // Client manages (modifies) the version information for the attestation variants.
type Client struct { type Client struct {
s3Client s3Client *apiclient.Client
s3ClientClose func(ctx context.Context) error s3ClientClose func(ctx context.Context) error
bucketID string bucketID string
cosignPwd []byte // used to decrypt the cosign private key signer sigstore.Signer
privKey []byte // used to sign fetcher fetcher.AttestationConfigAPIFetcher
} }
// New returns a new Client. // New returns a new Client.
func New(ctx context.Context, cfg staticupload.Config, cosignPwd, privateKey []byte) (*Client, CloseFunc, error) { func New(ctx context.Context, cfg staticupload.Config, cosignPwd, privateKey []byte, dryRun bool, log *logger.Logger) (*Client, apiclient.CloseFunc, error) {
client, clientClose, err := staticupload.New(ctx, cfg) s3Client, clientClose, err := apiclient.NewClient(ctx, cfg.Region, cfg.Bucket, cfg.DistributionID, dryRun, log)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to create s3 storage: %w", err) return nil, nil, fmt.Errorf("failed to create s3 storage: %w", err)
} }
repo := &Client{ repo := &Client{
s3Client: client, s3Client: s3Client,
s3ClientClose: clientClose, s3ClientClose: clientClose,
signer: sigstore.NewSigner(cosignPwd, privateKey),
bucketID: cfg.Bucket, bucketID: cfg.Bucket,
cosignPwd: cosignPwd, fetcher: fetcher.New(),
privKey: privateKey,
} }
repoClose := func(ctx context.Context) error { return repo, clientClose, nil
return repo.Close(ctx)
}
return repo, repoClose, nil
} }
// Close closes the Client. func (a Client) uploadAzureSEVSNP(versions attestationconfig.AzureSEVSNPVersion, versionNames []string, date time.Time) (res []putCmd, err error) {
func (a Client) Close(ctx context.Context) error { dateStr := date.Format("2006-01-02-15-04") + ".json"
if a.s3ClientClose == nil {
return nil res = append(res, putCmd{attestationconfig.AzureSEVSNPVersionAPI{Version: dateStr, AzureSEVSNPVersion: versions}})
versionBytes, err := json.Marshal(versions)
if err != nil {
return res, err
} }
return a.s3ClientClose(ctx) signature, err := a.createSignature(versionBytes, dateStr)
if err != nil {
return res, err
}
res = append(res, putCmd{signature})
newVersions := addVersion(versionNames, dateStr)
res = append(res, putCmd{attestationconfig.AzureSEVSNPVersionList(newVersions)})
return
} }
// UploadAzureSEVSNP uploads the latest version numbers of the Azure SEVSNP. // UploadAzureSEVSNP uploads the latest version numbers of the Azure SEVSNP.
func (a Client) UploadAzureSEVSNP(ctx context.Context, versions attestationconfig.AzureSEVSNPVersion, date time.Time) error { func (a Client) UploadAzureSEVSNP(ctx context.Context, version attestationconfig.AzureSEVSNPVersion, date time.Time) error {
versionBytes, err := json.Marshal(versions)
if err != nil {
return err
}
variant := variant.AzureSEVSNP{} variant := variant.AzureSEVSNP{}
fname := date.Format("2006-01-02-15-04") + ".json"
filePath := fmt.Sprintf("%s/%s/%s", constants.CDNAttestationConfigPrefixV1, variant.String(), fname) dateStr := date.Format("2006-01-02-15-04") + ".json"
err = put(ctx, a.s3Client, a.bucketID, filePath, versionBytes) err := apiclient.Update(ctx, a.s3Client, attestationconfig.AzureSEVSNPVersionAPI{Version: dateStr, AzureSEVSNPVersion: version})
if err != nil { if err != nil {
return err return err
} }
versionBytes, err := json.Marshal(version)
if err != nil {
return err
}
filePath := fmt.Sprintf("%s/%s/%s", constants.CDNAttestationConfigPrefixV1, variant.String(), dateStr)
err = a.createAndUploadSignature(ctx, versionBytes, filePath) err = a.createAndUploadSignature(ctx, versionBytes, filePath)
if err != nil { if err != nil {
return err return err
} }
return a.addVersionToList(ctx, variant, fname)
return a.addVersionToList(ctx, variant, dateStr)
}
func (a Client) createSignature(content []byte, dateStr string) (res attestationconfig.AzureSEVSNPVersionSignature, err error) {
signature, err := a.signer.Sign(content)
if err != nil {
return res, fmt.Errorf("sign version file: %w", err)
}
return attestationconfig.AzureSEVSNPVersionSignature{
Signature: signature,
Version: dateStr,
}, nil
} }
// createAndUploadSignature signs the given content and uploads the signature to the given filePath with the .sig suffix. // createAndUploadSignature signs the given content and uploads the signature to the given filePath with the .sig suffix.
func (a Client) createAndUploadSignature(ctx context.Context, content []byte, filePath string) error { func (a Client) createAndUploadSignature(ctx context.Context, content []byte, filePath string) error {
signature, err := sigstore.SignContent(a.cosignPwd, a.privKey, content) signature, err := a.createSignature(content, filePath)
if err != nil { if err != nil {
return fmt.Errorf("sign version file: %w", err) return err
} }
err = put(ctx, a.s3Client, a.bucketID, filePath+".sig", signature) if err := apiclient.Update(ctx, a.s3Client, signature); err != nil {
if err != nil {
return fmt.Errorf("upload signature: %w", err) return fmt.Errorf("upload signature: %w", err)
} }
return nil return nil
@ -99,81 +115,114 @@ func (a Client) createAndUploadSignature(ctx context.Context, content []byte, fi
// List returns the list of versions for the given attestation type. // List returns the list of versions for the given attestation type.
func (a Client) List(ctx context.Context, attestation variant.Variant) ([]string, error) { func (a Client) List(ctx context.Context, attestation variant.Variant) ([]string, error) {
key := path.Join(constants.CDNAttestationConfigPrefixV1, attestation.String(), "list") if attestation.Equal(variant.AzureSEVSNP{}) {
bt, err := get(ctx, a.s3Client, a.bucketID, key) versions, err := apiclient.Fetch(ctx, a.s3Client, attestationconfig.AzureSEVSNPVersionList{})
if err != nil { if err != nil {
return nil, err return nil, err
}
return versions, nil
} }
var versions []string return nil, fmt.Errorf("unsupported attestation type: %s", attestation)
if err := json.Unmarshal(bt, &versions); err != nil {
return nil, err
}
return versions, nil
} }
// DeleteList empties the list of versions for the given attestation type. // DeleteList empties the list of versions for the given attestation type.
func (a Client) DeleteList(ctx context.Context, attestation variant.Variant) error { func (a Client) DeleteList(ctx context.Context, attestation variant.Variant) error {
versions := []string{} if attestation.Equal(variant.AzureSEVSNP{}) {
bt, err := json.Marshal(&versions) return apiclient.Update(ctx, a.s3Client, attestationconfig.AzureSEVSNPVersionList{})
}
return fmt.Errorf("unsupported attestation type: %s", attestation)
}
func (a Client) deleteAzureSEVSNPVersion(versions attestationconfig.AzureSEVSNPVersionList, versionStr string) (ops []crudOPNew, err error) {
versionStr = versionStr + ".json"
ops = append(ops, deleteCmd{
apiObject: attestationconfig.AzureSEVSNPVersionAPI{
Version: versionStr,
},
})
ops = append(ops, deleteCmd{
apiObject: attestationconfig.AzureSEVSNPVersionSignature{
Version: versionStr,
},
})
removedVersions, err := removeVersion(versions, versionStr)
if err != nil {
return nil, err
}
ops = append(ops, putCmd{
apiObject: removedVersions,
})
return ops, nil
}
// DeleteAzureSEVSNPVersion deletes the given version (without .json suffix) from the API.
func (a Client) DeleteAzureSEVSNPVersion(ctx context.Context, versionStr string) error {
versions, err := a.List(ctx, variant.AzureSEVSNP{})
if err != nil {
return fmt.Errorf("fetch version list: %w", err)
}
ops, err := a.deleteAzureSEVSNPVersion(versions, versionStr)
if err != nil { if err != nil {
return err return err
} }
return put(ctx, a.s3Client, a.bucketID, path.Join(constants.CDNAttestationConfigPrefixV1, attestation.String(), "list"), bt) for _, op := range ops {
if err := op.Execute(ctx, a.s3Client); err != nil {
return fmt.Errorf("execute operation %+v: %w", op, err)
}
}
return nil
} }
func (a Client) addVersionToList(ctx context.Context, attestation variant.Variant, fname string) error { func (a Client) addVersionToList(ctx context.Context, attestation variant.Variant, fname string) error {
versions := []string{} versions, err := a.List(ctx, attestation)
key := path.Join(constants.CDNAttestationConfigPrefixV1, attestation.String(), "list") if err != nil {
bt, err := get(ctx, a.s3Client, a.bucketID, key)
if err == nil {
if err := json.Unmarshal(bt, &versions); err != nil {
return err
}
} else if !errors.Is(err, storage.ErrDEKUnset) {
return err return err
} }
versions = append(versions, fname) versions = append(versions, fname)
versions = variant.RemoveDuplicate(versions) versions = variant.RemoveDuplicate(versions)
sort.Sort(sort.Reverse(sort.StringSlice(versions))) sort.Sort(sort.Reverse(sort.StringSlice(versions)))
json, err := json.Marshal(versions) return apiclient.Update(ctx, a.s3Client, attestationconfig.AzureSEVSNPVersionList(versions))
if err != nil {
return err
}
return put(ctx, a.s3Client, a.bucketID, key, json)
} }
// get is a convenience method. func removeVersion(versions attestationconfig.AzureSEVSNPVersionList, versionStr string) (removedVersions attestationconfig.AzureSEVSNPVersionList, err error) {
func get(ctx context.Context, client s3Client, bucket, path string) ([]byte, error) { for i, v := range versions {
getObjectInput := &s3.GetObjectInput{ if v == versionStr {
Bucket: &bucket, if i == len(versions)-1 {
Key: &path, removedVersions = versions[:i]
} else {
removedVersions = append(versions[:i], versions[i+1:]...)
}
return removedVersions, nil
}
} }
output, err := client.GetObject(ctx, getObjectInput) return nil, fmt.Errorf("version %s not found in list %v", versionStr, versions)
if err != nil {
return nil, fmt.Errorf("getting object: %w", err)
}
return io.ReadAll(output.Body)
} }
// put is a convenience method. type deleteCmd struct {
func put(ctx context.Context, client s3Client, bucket, path string, data []byte) error { apiObject apiclient.APIObject
putObjectInput := &s3.PutObjectInput{
Bucket: &bucket,
Key: &path,
Body: bytes.NewReader(data),
}
_, err := client.Upload(ctx, putObjectInput)
return err
} }
type s3Client interface { func (d deleteCmd) Execute(ctx context.Context, c *apiclient.Client) error {
GetObject( return apiclient.Delete(ctx, c, d.apiObject)
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 putCmd struct {
type CloseFunc func(ctx context.Context) error apiObject apiclient.APIObject
}
func (p putCmd) Execute(ctx context.Context, c *apiclient.Client) error {
return apiclient.Update(ctx, c, p.apiObject)
}
type crudOPNew interface {
Execute(ctx context.Context, c *apiclient.Client) error
}
func addVersion(versions []string, newVersion string) []string {
versions = append(versions, newVersion)
versions = variant.RemoveDuplicate(versions)
sort.Sort(sort.Reverse(sort.StringSlice(versions)))
return versions
}

View File

@ -1,82 +1,74 @@
//go:build e2e
/* /*
Copyright (c) Edgeless Systems GmbH Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
*/ */
package client_test package client
import ( import (
"context"
"flag"
"fmt"
"io"
"os"
"testing" "testing"
"time" "time"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig" "github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/client" "github.com/stretchr/testify/assert"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
"github.com/stretchr/testify/require"
) )
const ( func TestUploadAzureSEVSNP(t *testing.T) {
awsBucket = "cdn-constellation-backend" sut := Client{
awsRegion = "eu-central-1" bucketID: "bucket",
envAwsKeyID = "AWS_ACCESS_KEY_ID" signer: fakeSigner{},
envAwsKey = "AWS_ACCESS_KEY"
)
var cfg staticupload.Config
var (
cosignPwd = flag.String("cosign-pwd", "", "Password to decrypt the cosign private key. Required for signing.")
privateKeyPath = flag.String("private-key", "", "Path to the private key used for signing. Required for signing.")
privateKey []byte
)
func TestMain(m *testing.M) {
flag.Parse()
if *cosignPwd == "" || *privateKeyPath == "" {
flag.Usage()
fmt.Println("Required flags not set: --cosign-pwd, --private-key. Skipping tests.")
os.Exit(1)
} }
if _, present := os.LookupEnv(envAwsKey); !present { version := attestationconfig.AzureSEVSNPVersion{}
fmt.Printf("%s not set. Skipping tests.\n", envAwsKey) date := time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC)
os.Exit(1) ops, err := sut.uploadAzureSEVSNP(version, []string{"2021-01-01-01-01.json", "2019-01-01-01-01.json"}, date)
} assert := assert.New(t)
if _, present := os.LookupEnv(envAwsKeyID); !present { assert.NoError(err)
fmt.Printf("%s not set. Skipping tests.\n", envAwsKeyID) dateStr := "2023-01-01-01-01.json"
os.Exit(1) assert.Contains(ops, putCmd{
} apiObject: attestationconfig.AzureSEVSNPVersionAPI{
cfg = staticupload.Config{ Version: dateStr,
Bucket: awsBucket, AzureSEVSNPVersion: version,
Region: awsRegion, },
} })
file, _ := os.Open(*privateKeyPath) assert.Contains(ops, putCmd{
var err error apiObject: attestationconfig.AzureSEVSNPVersionSignature{
privateKey, err = io.ReadAll(file) Version: dateStr,
if err != nil { Signature: []byte("signature"),
panic(err) },
} })
os.Exit(m.Run()) assert.Contains(ops, putCmd{
apiObject: attestationconfig.AzureSEVSNPVersionList([]string{"2023-01-01-01-01.json", "2021-01-01-01-01.json", "2019-01-01-01-01.json"}),
})
} }
var versionValues = attestationconfig.AzureSEVSNPVersion{ func TestDeleteAzureSEVSNPVersions(t *testing.T) {
Bootloader: 2, sut := Client{
TEE: 0, bucketID: "bucket",
SNP: 6, }
Microcode: 93, versions := attestationconfig.AzureSEVSNPVersionList([]string{"2023-01-01.json", "2021-01-01.json", "2019-01-01.json"})
ops, err := sut.deleteAzureSEVSNPVersion(versions, "2021-01-01")
assert := assert.New(t)
assert.NoError(err)
assert.Contains(ops, deleteCmd{
apiObject: attestationconfig.AzureSEVSNPVersionAPI{
Version: "2021-01-01.json",
},
})
assert.Contains(ops, deleteCmd{
apiObject: attestationconfig.AzureSEVSNPVersionSignature{
Version: "2021-01-01.json",
},
})
assert.Contains(ops, putCmd{
apiObject: attestationconfig.AzureSEVSNPVersionList([]string{"2023-01-01.json", "2019-01-01.json"}),
})
} }
func TestUploadAzureSEVSNPVersions(t *testing.T) { type fakeSigner struct{}
ctx := context.Background()
client, clientClose, err := client.New(ctx, cfg, []byte(*cosignPwd), privateKey) func (fakeSigner) Sign(_ []byte) ([]byte, error) {
require.NoError(t, err) return []byte("signature"), nil
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

@ -0,0 +1,84 @@
//go:build integration
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package test
import (
"context"
"flag"
"fmt"
"io"
"os"
"testing"
"time"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfig/client"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/edgelesssys/constellation/v2/internal/staticupload"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)
const (
awsBucket = "cdn-constellation-backend"
awsRegion = "eu-central-1"
envAwsKeyID = "AWS_ACCESS_KEY_ID"
envAwsKey = "AWS_ACCESS_KEY"
)
var cfg staticupload.Config
var (
cosignPwd = flag.String("cosign-pwd", "", "Password to decrypt the cosign private key. Required for signing.")
privateKeyPath = flag.String("private-key", "", "Path to the private key used for signing. Required for signing.")
privateKey []byte
)
func TestMain(m *testing.M) {
flag.Parse()
if *cosignPwd == "" || *privateKeyPath == "" {
flag.Usage()
fmt.Println("Required flags not set: --cosign-pwd, --private-key. Skipping tests.")
os.Exit(1)
}
if _, present := os.LookupEnv(envAwsKey); !present {
fmt.Printf("%s not set. Skipping tests.\n", envAwsKey)
os.Exit(1)
}
if _, present := os.LookupEnv(envAwsKeyID); !present {
fmt.Printf("%s not set. Skipping tests.\n", envAwsKeyID)
os.Exit(1)
}
cfg = staticupload.Config{
Bucket: awsBucket,
Region: awsRegion,
}
file, _ := os.Open(*privateKeyPath)
var err error
privateKey, err = io.ReadAll(file)
if err != nil {
panic(err)
}
os.Exit(m.Run())
}
var versionValues = attestationconfig.AzureSEVSNPVersion{
Bootloader: 2,
TEE: 0,
SNP: 6,
Microcode: 93,
}
func TestUploadAzureSEVSNPVersions(t *testing.T) {
ctx := context.Background()
client, clientClose, err := client.New(ctx, cfg, []byte(*cosignPwd), privateKey, false, logger.New(logger.PlainLog, zap.DebugLevel).Named("attestationconfig"))
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

@ -24,9 +24,9 @@ const cosignPublicKey = constants.CosignPublicKeyReleases
// AttestationConfigAPIFetcher fetches config API resources without authentication. // AttestationConfigAPIFetcher fetches config API resources without authentication.
type AttestationConfigAPIFetcher interface { type AttestationConfigAPIFetcher interface {
FetchAzureSEVSNPVersion(ctx context.Context, azureVersion attestationconfig.AzureSEVSNPVersionGet) (attestationconfig.AzureSEVSNPVersionGet, error) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion attestationconfig.AzureSEVSNPVersionAPI) (attestationconfig.AzureSEVSNPVersionAPI, error)
FetchAzureSEVSNPVersionList(ctx context.Context, attestation attestationconfig.AzureSEVSNPVersionList) (attestationconfig.AzureSEVSNPVersionList, error) FetchAzureSEVSNPVersionList(ctx context.Context, attestation attestationconfig.AzureSEVSNPVersionList) (attestationconfig.AzureSEVSNPVersionList, error)
FetchAzureSEVSNPVersionLatest(ctx context.Context) (attestationconfig.AzureSEVSNPVersionGet, error) FetchAzureSEVSNPVersionLatest(ctx context.Context) (attestationconfig.AzureSEVSNPVersionAPI, error)
} }
// Fetcher fetches AttestationCfg API resources without authentication. // Fetcher fetches AttestationCfg API resources without authentication.
@ -50,7 +50,7 @@ func (f *Fetcher) FetchAzureSEVSNPVersionList(ctx context.Context, attestation a
} }
// FetchAzureSEVSNPVersion fetches the version information from the config API. // FetchAzureSEVSNPVersion fetches the version information from the config API.
func (f *Fetcher) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion attestationconfig.AzureSEVSNPVersionGet) (attestationconfig.AzureSEVSNPVersionGet, error) { func (f *Fetcher) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion attestationconfig.AzureSEVSNPVersionAPI) (attestationconfig.AzureSEVSNPVersionAPI, error) {
urlString, err := azureVersion.URL() urlString, err := azureVersion.URL()
if err != nil { if err != nil {
return azureVersion, err return azureVersion, err
@ -77,13 +77,13 @@ func (f *Fetcher) FetchAzureSEVSNPVersion(ctx context.Context, azureVersion atte
} }
// FetchAzureSEVSNPVersionLatest returns the latest versions of the given type. // FetchAzureSEVSNPVersionLatest returns the latest versions of the given type.
func (f *Fetcher) FetchAzureSEVSNPVersionLatest(ctx context.Context) (res attestationconfig.AzureSEVSNPVersionGet, err error) { func (f *Fetcher) FetchAzureSEVSNPVersionLatest(ctx context.Context) (res attestationconfig.AzureSEVSNPVersionAPI, err error) {
var list attestationconfig.AzureSEVSNPVersionList var list attestationconfig.AzureSEVSNPVersionList
list, err = f.FetchAzureSEVSNPVersionList(ctx, list) list, err = f.FetchAzureSEVSNPVersionList(ctx, list)
if err != nil { if err != nil {
return res, fmt.Errorf("fetching versions list: %w", err) return res, fmt.Errorf("fetching versions list: %w", err)
} }
get := attestationconfig.AzureSEVSNPVersionGet{Version: list[0]} // get latest version (as sorted reversely alphanumerically) get := attestationconfig.AzureSEVSNPVersionAPI{Version: list[0]} // get latest version (as sorted reversely alphanumerically)
get, err = f.FetchAzureSEVSNPVersion(ctx, get) get, err = f.FetchAzureSEVSNPVersion(ctx, get)
if err != nil { if err != nil {
return res, fmt.Errorf("failed fetching version: %w", err) return res, fmt.Errorf("failed fetching version: %w", err)

View File

@ -18,7 +18,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
var testCfg = configapi.AzureSEVSNPVersionGet{ var testCfg = configapi.AzureSEVSNPVersionAPI{
AzureSEVSNPVersion: configapi.AzureSEVSNPVersion{ AzureSEVSNPVersion: configapi.AzureSEVSNPVersion{
Microcode: 93, Microcode: 93,
TEE: 0, TEE: 0,
@ -31,7 +31,7 @@ func TestFetchLatestAzureSEVSNPVersion(t *testing.T) {
testcases := map[string]struct { testcases := map[string]struct {
signature []byte signature []byte
wantErr bool wantErr bool
want configapi.AzureSEVSNPVersionGet want configapi.AzureSEVSNPVersionAPI
}{ }{
"get version with valid signature": { "get version with valid signature": {
signature: []byte("MEQCIBPEbYg89MIQuaGStLhKGLGMKvKFoYCaAniDLwoIwulqAiB+rj7KMaMOMGxmUsjI7KheCXSNM8NzN+tuDw6AywI75A=="), // signed with release key signature: []byte("MEQCIBPEbYg89MIQuaGStLhKGLGMKvKFoYCaAniDLwoIwulqAiB+rj7KMaMOMGxmUsjI7KheCXSNM8NzN+tuDw6AywI75A=="), // signed with release key

View File

@ -43,10 +43,9 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
// Client is the client for the versions API. // Client is the a general client for all APIs.
type Client struct { type Client struct {
uploadClient uploadClient s3Client
s3Client s3Client
s3ClientClose func(ctx context.Context) error s3ClientClose func(ctx context.Context) error
bucket string bucket string
cacheInvalidationWaitTimeout time.Duration cacheInvalidationWaitTimeout time.Duration
@ -101,7 +100,6 @@ func NewClient(ctx context.Context, region, bucket, distributionID string, dryRu
} }
client := &Client{ client := &Client{
uploadClient: staticUploadClient,
s3Client: staticUploadClient, s3Client: staticUploadClient,
s3ClientClose: staticUploadClientClose, s3ClientClose: staticUploadClientClose,
bucket: bucket, bucket: bucket,
@ -179,14 +177,15 @@ func ptr[T any](t T) *T {
return &t return &t
} }
type apiObject interface { // APIObject is an object that is used to perform CRUD operations on the API.
type APIObject interface {
ValidateRequest() error ValidateRequest() error
Validate() error Validate() error
JSONPath() string JSONPath() string
} }
// Fetch fetches the given apiObject from the public Constellation CDN. // Fetch fetches the given apiObject from the public Constellation CDN.
func Fetch[T apiObject](ctx context.Context, c *Client, obj T) (T, error) { func Fetch[T APIObject](ctx context.Context, c *Client, obj T) (T, error) {
if err := obj.ValidateRequest(); err != nil { if err := obj.ValidateRequest(); err != nil {
return *new(T), fmt.Errorf("validating request for %T: %w", obj, err) return *new(T), fmt.Errorf("validating request for %T: %w", obj, err)
} }
@ -218,8 +217,8 @@ func Fetch[T apiObject](ctx context.Context, c *Client, obj T) (T, error) {
return newObj, nil return newObj, nil
} }
// Update creates/updates the given apiObject in the public Constellation CDN. // Update creates/updates the given apiObject in the public Constellation API.
func Update[T apiObject](ctx context.Context, c *Client, obj T) error { func Update(ctx context.Context, c *Client, obj APIObject) error {
if err := obj.Validate(); err != nil { if err := obj.Validate(); err != nil {
return fmt.Errorf("validating %T struct: %w", obj, err) return fmt.Errorf("validating %T struct: %w", obj, err)
} }
@ -243,13 +242,32 @@ func Update[T apiObject](ctx context.Context, c *Client, obj T) error {
c.dirtyPaths = append(c.dirtyPaths, "/"+obj.JSONPath()) c.dirtyPaths = append(c.dirtyPaths, "/"+obj.JSONPath())
c.Log.Debugf("Uploading %T to s3: %v", obj, obj.JSONPath()) c.Log.Debugf("Uploading %T to s3: %v", obj, obj.JSONPath())
if _, err := c.uploadClient.Upload(ctx, in); err != nil { if _, err := c.Upload(ctx, in); err != nil {
return fmt.Errorf("uploading %T: %w", obj, err) return fmt.Errorf("uploading %T: %w", obj, err)
} }
return nil return nil
} }
// Delete deletes the given apiObject from the public Constellation API.
func Delete(ctx context.Context, c *Client, obj APIObject) error {
if err := obj.ValidateRequest(); err != nil {
return fmt.Errorf("validating request for %T: %w", obj, err)
}
in := &s3.DeleteObjectInput{
Bucket: &c.bucket,
Key: ptr(obj.JSONPath()),
}
c.Log.Debugf("Deleting %T from s3: %s", obj, obj.JSONPath())
if _, err := c.DeleteObject(ctx, in); err != nil {
return fmt.Errorf("deleting s3 object at %s: %w", obj.JSONPath(), err)
}
return nil
}
// NotFoundError is an error that is returned when a resource is not found. // NotFoundError is an error that is returned when a resource is not found.
type NotFoundError struct { type NotFoundError struct {
err error err error
@ -273,6 +291,10 @@ type s3Client interface {
DeleteObjects( DeleteObjects(
ctx context.Context, params *s3.DeleteObjectsInput, optFns ...func(*s3.Options), ctx context.Context, params *s3.DeleteObjectsInput, optFns ...func(*s3.Options),
) (*s3.DeleteObjectsOutput, error) ) (*s3.DeleteObjectsOutput, error)
DeleteObject(ctx context.Context, params *s3.DeleteObjectInput,
optFns ...func(*s3.Options),
) (*s3.DeleteObjectOutput, error)
uploadClient
} }
type uploadClient interface { type uploadClient interface {

View File

@ -900,14 +900,14 @@ func (f fakeConfigFetcher) FetchAzureSEVSNPVersionList(_ context.Context, _ conf
), nil ), nil
} }
func (f fakeConfigFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ configapi.AzureSEVSNPVersionGet) (configapi.AzureSEVSNPVersionGet, error) { func (f fakeConfigFetcher) FetchAzureSEVSNPVersion(_ context.Context, _ configapi.AzureSEVSNPVersionAPI) (configapi.AzureSEVSNPVersionAPI, error) {
return configapi.AzureSEVSNPVersionGet{ return configapi.AzureSEVSNPVersionAPI{
AzureSEVSNPVersion: testCfg, AzureSEVSNPVersion: testCfg,
}, nil }, nil
} }
func (f fakeConfigFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (configapi.AzureSEVSNPVersionGet, error) { func (f fakeConfigFetcher) FetchAzureSEVSNPVersionLatest(_ context.Context) (configapi.AzureSEVSNPVersionAPI, error) {
return configapi.AzureSEVSNPVersionGet{ return configapi.AzureSEVSNPVersionAPI{
AzureSEVSNPVersion: testCfg, AzureSEVSNPVersion: testCfg,
}, nil }, nil
} }

View File

@ -26,6 +26,29 @@ const (
sigstorePrivateKeyPemType = "ENCRYPTED SIGSTORE PRIVATE KEY" sigstorePrivateKeyPemType = "ENCRYPTED SIGSTORE PRIVATE KEY"
) )
// Signer is used to sign the version file. Used for unit testing.
type Signer interface {
Sign(content []byte) (res []byte, err error)
}
// NewSigner returns a new Signer.
func NewSigner(cosignPwd, privKey []byte) Signer {
return signer{cosignPwd: cosignPwd, privKey: privKey}
}
type signer struct {
cosignPwd []byte // used to decrypt the cosign private key
privKey []byte // used to sign
}
func (s signer) Sign(content []byte) (signature []byte, err error) {
signature, err = SignContent(s.cosignPwd, s.privKey, content)
if err != nil {
return signature, fmt.Errorf("sign version file: %w", err)
}
return
}
// SignContent signs the content with the cosign encrypted private key and corresponding cosign password. // SignContent signs the content with the cosign encrypted private key and corresponding cosign password.
func SignContent(password, encryptedPrivateKey, content []byte) ([]byte, error) { func SignContent(password, encryptedPrivateKey, content []byte) ([]byte, error) {
sv, err := loadPrivateKey(encryptedPrivateKey, password) sv, err := loadPrivateKey(encryptedPrivateKey, password)

View File

@ -93,7 +93,7 @@ func (e InvalidationError) Unwrap() error {
return e.inner return e.inner
} }
// New creates a new Client. // New creates a new Client. Call CloseFunc when done with operations.
func New(ctx context.Context, config Config) (*Client, CloseFunc, error) { func New(ctx context.Context, config Config) (*Client, CloseFunc, error) {
config.SetsDefault() config.SetsDefault()
cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(config.Region)) cfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(config.Region))
@ -114,12 +114,7 @@ func New(ctx context.Context, config Config) (*Client, CloseFunc, error) {
cacheInvalidationWaitTimeout: config.CacheInvalidationWaitTimeout, cacheInvalidationWaitTimeout: config.CacheInvalidationWaitTimeout,
bucketID: config.Bucket, bucketID: config.Bucket,
} }
clientClose := func(ctx context.Context) error { return client, client.Flush, nil
// ensure that all keys are invalidated
return client.Flush(ctx)
}
return client, clientClose, nil
} }
// Flush flushes the client by invalidating the CDN cache for modified keys. // Flush flushes the client by invalidating the CDN cache for modified keys.