mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-01-22 21:31:14 -05:00
Move upload/delete code to its own package
Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
parent
5654e76f7e
commit
52a65c20ac
@ -5,31 +5,23 @@ go_library(
|
|||||||
name = "attestationconfigapi",
|
name = "attestationconfigapi",
|
||||||
srcs = [
|
srcs = [
|
||||||
"attestationconfigapi.go",
|
"attestationconfigapi.go",
|
||||||
"client.go",
|
|
||||||
"fetcher.go",
|
"fetcher.go",
|
||||||
"reporter.go",
|
|
||||||
"snp.go",
|
"snp.go",
|
||||||
],
|
],
|
||||||
importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi",
|
importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi",
|
||||||
visibility = ["//:__subpackages__"],
|
visibility = ["//:__subpackages__"],
|
||||||
deps = [
|
deps = [
|
||||||
"//internal/api/client",
|
|
||||||
"//internal/api/fetcher",
|
"//internal/api/fetcher",
|
||||||
"//internal/attestation/variant",
|
"//internal/attestation/variant",
|
||||||
"//internal/constants",
|
"//internal/constants",
|
||||||
"//internal/sigstore",
|
"//internal/sigstore",
|
||||||
"//internal/staticupload",
|
|
||||||
"@com_github_aws_aws_sdk_go//aws",
|
|
||||||
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
go_test(
|
go_test(
|
||||||
name = "attestationconfigapi_test",
|
name = "attestationconfigapi_test",
|
||||||
srcs = [
|
srcs = [
|
||||||
"client_test.go",
|
|
||||||
"fetcher_test.go",
|
"fetcher_test.go",
|
||||||
"reporter_test.go",
|
|
||||||
"snp_test.go",
|
"snp_test.go",
|
||||||
],
|
],
|
||||||
embed = [":attestationconfigapi"],
|
embed = [":attestationconfigapi"],
|
||||||
|
@ -19,6 +19,7 @@ go_library(
|
|||||||
visibility = ["//visibility:private"],
|
visibility = ["//visibility:private"],
|
||||||
deps = [
|
deps = [
|
||||||
"//internal/api/attestationconfigapi",
|
"//internal/api/attestationconfigapi",
|
||||||
|
"//internal/api/attestationconfigapi/cli/client",
|
||||||
"//internal/attestation/variant",
|
"//internal/attestation/variant",
|
||||||
"//internal/cloud/cloudprovider",
|
"//internal/cloud/cloudprovider",
|
||||||
"//internal/constants",
|
"//internal/constants",
|
||||||
|
35
internal/api/attestationconfigapi/cli/client/BUILD.bazel
Normal file
35
internal/api/attestationconfigapi/cli/client/BUILD.bazel
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||||
|
load("//bazel/go:go_test.bzl", "go_test")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "client",
|
||||||
|
srcs = [
|
||||||
|
"client.go",
|
||||||
|
"reporter.go",
|
||||||
|
],
|
||||||
|
importpath = "github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli/client",
|
||||||
|
visibility = ["//:__subpackages__"],
|
||||||
|
deps = [
|
||||||
|
"//internal/api/attestationconfigapi",
|
||||||
|
"//internal/api/client",
|
||||||
|
"//internal/attestation/variant",
|
||||||
|
"//internal/sigstore",
|
||||||
|
"//internal/staticupload",
|
||||||
|
"@com_github_aws_aws_sdk_go//aws",
|
||||||
|
"@com_github_aws_aws_sdk_go_v2_service_s3//:s3",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
go_test(
|
||||||
|
name = "client_test",
|
||||||
|
srcs = [
|
||||||
|
"client_test.go",
|
||||||
|
"reporter_test.go",
|
||||||
|
],
|
||||||
|
embed = [":client"],
|
||||||
|
deps = [
|
||||||
|
"//internal/api/attestationconfigapi",
|
||||||
|
"//internal/attestation/variant",
|
||||||
|
"@com_github_stretchr_testify//assert",
|
||||||
|
],
|
||||||
|
)
|
@ -3,7 +3,12 @@ Copyright (c) Edgeless Systems GmbH
|
|||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
package attestationconfigapi
|
|
||||||
|
/*
|
||||||
|
package client contains code to manage CVM versions in Constellation's CDN API.
|
||||||
|
It is used to upload and delete "latest" versions for AMD SEV-SNP and Intel TDX.
|
||||||
|
*/
|
||||||
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -12,6 +17,7 @@ import (
|
|||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
||||||
apiclient "github.com/edgelesssys/constellation/v2/internal/api/client"
|
apiclient "github.com/edgelesssys/constellation/v2/internal/api/client"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
"github.com/edgelesssys/constellation/v2/internal/sigstore"
|
||||||
@ -31,8 +37,8 @@ type Client struct {
|
|||||||
cacheWindowSize int
|
cacheWindowSize int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns a new Client.
|
// New returns a new Client.
|
||||||
func NewClient(ctx context.Context, cfg staticupload.Config, cosignPwd, privateKey []byte, dryRun bool, versionWindowSize int, log *slog.Logger) (*Client, apiclient.CloseFunc, error) {
|
func New(ctx context.Context, cfg staticupload.Config, cosignPwd, privateKey []byte, dryRun bool, versionWindowSize int, log *slog.Logger) (*Client, apiclient.CloseFunc, error) {
|
||||||
s3Client, clientClose, err := apiclient.NewClient(ctx, cfg.Region, cfg.Bucket, cfg.DistributionID, dryRun, log)
|
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)
|
||||||
@ -49,7 +55,7 @@ func NewClient(ctx context.Context, cfg staticupload.Config, cosignPwd, privateK
|
|||||||
}
|
}
|
||||||
|
|
||||||
// uploadSEVSNPVersion uploads the latest version numbers of the SEVSNP. Then version name is the UTC timestamp of the date. The /list entry stores the version name + .json suffix.
|
// uploadSEVSNPVersion uploads the latest version numbers of the SEVSNP. Then version name is the UTC timestamp of the date. The /list entry stores the version name + .json suffix.
|
||||||
func (a Client) uploadSEVSNPVersion(ctx context.Context, attestation variant.Variant, version SEVSNPVersion, date time.Time) error {
|
func (a Client) uploadSEVSNPVersion(ctx context.Context, attestation variant.Variant, version attestationconfigapi.SEVSNPVersion, date time.Time) error {
|
||||||
versions, err := a.List(ctx, attestation)
|
versions, err := a.List(ctx, attestation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("fetch version list: %w", err)
|
return fmt.Errorf("fetch version list: %w", err)
|
||||||
@ -74,32 +80,32 @@ func (a Client) DeleteSEVSNPVersion(ctx context.Context, attestation variant.Var
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List returns the list of versions for the given attestation variant.
|
// List returns the list of versions for the given attestation variant.
|
||||||
func (a Client) List(ctx context.Context, attestation variant.Variant) (SEVSNPVersionList, error) {
|
func (a Client) List(ctx context.Context, attestation variant.Variant) (attestationconfigapi.SEVSNPVersionList, error) {
|
||||||
if !attestation.Equal(variant.AzureSEVSNP{}) &&
|
if !attestation.Equal(variant.AzureSEVSNP{}) &&
|
||||||
!attestation.Equal(variant.AWSSEVSNP{}) &&
|
!attestation.Equal(variant.AWSSEVSNP{}) &&
|
||||||
!attestation.Equal(variant.GCPSEVSNP{}) {
|
!attestation.Equal(variant.GCPSEVSNP{}) {
|
||||||
return SEVSNPVersionList{}, fmt.Errorf("unsupported attestation variant: %s", attestation)
|
return attestationconfigapi.SEVSNPVersionList{}, fmt.Errorf("unsupported attestation variant: %s", attestation)
|
||||||
}
|
}
|
||||||
|
|
||||||
versions, err := apiclient.Fetch(ctx, a.s3Client, SEVSNPVersionList{variant: attestation})
|
versions, err := apiclient.Fetch(ctx, a.s3Client, attestationconfigapi.SEVSNPVersionList{Variant: attestation})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var notFoundErr *apiclient.NotFoundError
|
var notFoundErr *apiclient.NotFoundError
|
||||||
if errors.As(err, ¬FoundErr) {
|
if errors.As(err, ¬FoundErr) {
|
||||||
return SEVSNPVersionList{variant: attestation}, nil
|
return attestationconfigapi.SEVSNPVersionList{Variant: attestation}, nil
|
||||||
}
|
}
|
||||||
return SEVSNPVersionList{}, err
|
return attestationconfigapi.SEVSNPVersionList{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
versions.variant = attestation
|
versions.Variant = attestation
|
||||||
|
|
||||||
return versions, nil
|
return versions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Client) deleteSEVSNPVersion(versions SEVSNPVersionList, versionStr string) (ops []crudCmd, err error) {
|
func (a Client) deleteSEVSNPVersion(versions attestationconfigapi.SEVSNPVersionList, versionStr string) (ops []crudCmd, err error) {
|
||||||
versionStr = versionStr + ".json"
|
versionStr = versionStr + ".json"
|
||||||
ops = append(ops, deleteCmd{
|
ops = append(ops, deleteCmd{
|
||||||
apiObject: SEVSNPVersionAPI{
|
apiObject: attestationconfigapi.SEVSNPVersionAPI{
|
||||||
Variant: versions.variant,
|
Variant: versions.Variant,
|
||||||
Version: versionStr,
|
Version: versionStr,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -115,8 +121,8 @@ func (a Client) deleteSEVSNPVersion(versions SEVSNPVersionList, versionStr strin
|
|||||||
return ops, nil
|
return ops, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a Client) constructUploadCmd(attestation variant.Variant, version SEVSNPVersion, versionNames SEVSNPVersionList, date time.Time) []crudCmd {
|
func (a Client) constructUploadCmd(attestation variant.Variant, version attestationconfigapi.SEVSNPVersion, versionNames attestationconfigapi.SEVSNPVersionList, date time.Time) []crudCmd {
|
||||||
if !attestation.Equal(versionNames.variant) {
|
if !attestation.Equal(versionNames.Variant) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,11 +130,11 @@ func (a Client) constructUploadCmd(attestation variant.Variant, version SEVSNPVe
|
|||||||
var res []crudCmd
|
var res []crudCmd
|
||||||
|
|
||||||
res = append(res, putCmd{
|
res = append(res, putCmd{
|
||||||
apiObject: SEVSNPVersionAPI{Version: dateStr, Variant: attestation, SEVSNPVersion: version},
|
apiObject: attestationconfigapi.SEVSNPVersionAPI{Version: dateStr, Variant: attestation, SEVSNPVersion: version},
|
||||||
signer: a.signer,
|
signer: a.signer,
|
||||||
})
|
})
|
||||||
|
|
||||||
versionNames.addVersion(dateStr)
|
versionNames.AddVersion(dateStr)
|
||||||
|
|
||||||
res = append(res, putCmd{
|
res = append(res, putCmd{
|
||||||
apiObject: versionNames,
|
apiObject: versionNames,
|
||||||
@ -138,19 +144,19 @@ func (a Client) constructUploadCmd(attestation variant.Variant, version SEVSNPVe
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeVersion(list SEVSNPVersionList, versionStr string) (removedVersions SEVSNPVersionList, err error) {
|
func removeVersion(list attestationconfigapi.SEVSNPVersionList, versionStr string) (removedVersions attestationconfigapi.SEVSNPVersionList, err error) {
|
||||||
versions := list.List()
|
versions := list.List
|
||||||
for i, v := range versions {
|
for i, v := range versions {
|
||||||
if v == versionStr {
|
if v == versionStr {
|
||||||
if i == len(versions)-1 {
|
if i == len(versions)-1 {
|
||||||
removedVersions = SEVSNPVersionList{list: versions[:i], variant: list.variant}
|
removedVersions = attestationconfigapi.SEVSNPVersionList{List: versions[:i], Variant: list.Variant}
|
||||||
} else {
|
} else {
|
||||||
removedVersions = SEVSNPVersionList{list: append(versions[:i], versions[i+1:]...), variant: list.variant}
|
removedVersions = attestationconfigapi.SEVSNPVersionList{List: append(versions[:i], versions[i+1:]...), Variant: list.Variant}
|
||||||
}
|
}
|
||||||
return removedVersions, nil
|
return removedVersions, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return SEVSNPVersionList{}, fmt.Errorf("version %s not found in list %v", versionStr, versions)
|
return attestationconfigapi.SEVSNPVersionList{}, fmt.Errorf("version %s not found in list %v", versionStr, versions)
|
||||||
}
|
}
|
||||||
|
|
||||||
type crudCmd interface {
|
type crudCmd interface {
|
@ -3,12 +3,13 @@ Copyright (c) Edgeless Systems GmbH
|
|||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
package attestationconfigapi
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -18,13 +19,13 @@ func TestUploadAzureSEVSNP(t *testing.T) {
|
|||||||
bucketID: "bucket",
|
bucketID: "bucket",
|
||||||
signer: fakeSigner{},
|
signer: fakeSigner{},
|
||||||
}
|
}
|
||||||
version := SEVSNPVersion{}
|
version := attestationconfigapi.SEVSNPVersion{}
|
||||||
date := time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC)
|
date := time.Date(2023, 1, 1, 1, 1, 1, 1, time.UTC)
|
||||||
ops := sut.constructUploadCmd(variant.AzureSEVSNP{}, version, SEVSNPVersionList{list: []string{"2021-01-01-01-01.json", "2019-01-01-01-01.json"}, variant: variant.AzureSEVSNP{}}, date)
|
ops := sut.constructUploadCmd(variant.AzureSEVSNP{}, version, attestationconfigapi.SEVSNPVersionList{List: []string{"2021-01-01-01-01.json", "2019-01-01-01-01.json"}, Variant: variant.AzureSEVSNP{}}, date)
|
||||||
dateStr := "2023-01-01-01-01.json"
|
dateStr := "2023-01-01-01-01.json"
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
assert.Contains(ops, putCmd{
|
assert.Contains(ops, putCmd{
|
||||||
apiObject: SEVSNPVersionAPI{
|
apiObject: attestationconfigapi.SEVSNPVersionAPI{
|
||||||
Variant: variant.AzureSEVSNP{},
|
Variant: variant.AzureSEVSNP{},
|
||||||
Version: dateStr,
|
Version: dateStr,
|
||||||
SEVSNPVersion: version,
|
SEVSNPVersion: version,
|
||||||
@ -32,7 +33,7 @@ func TestUploadAzureSEVSNP(t *testing.T) {
|
|||||||
signer: fakeSigner{},
|
signer: fakeSigner{},
|
||||||
})
|
})
|
||||||
assert.Contains(ops, putCmd{
|
assert.Contains(ops, putCmd{
|
||||||
apiObject: SEVSNPVersionList{variant: variant.AzureSEVSNP{}, list: []string{"2023-01-01-01-01.json", "2021-01-01-01-01.json", "2019-01-01-01-01.json"}},
|
apiObject: attestationconfigapi.SEVSNPVersionList{Variant: variant.AzureSEVSNP{}, List: []string{"2023-01-01-01-01.json", "2021-01-01-01-01.json", "2019-01-01-01-01.json"}},
|
||||||
signer: fakeSigner{},
|
signer: fakeSigner{},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -41,20 +42,20 @@ func TestDeleteAzureSEVSNPVersions(t *testing.T) {
|
|||||||
sut := Client{
|
sut := Client{
|
||||||
bucketID: "bucket",
|
bucketID: "bucket",
|
||||||
}
|
}
|
||||||
versions := SEVSNPVersionList{list: []string{"2023-01-01.json", "2021-01-01.json", "2019-01-01.json"}}
|
versions := attestationconfigapi.SEVSNPVersionList{List: []string{"2023-01-01.json", "2021-01-01.json", "2019-01-01.json"}}
|
||||||
|
|
||||||
ops, err := sut.deleteSEVSNPVersion(versions, "2021-01-01")
|
ops, err := sut.deleteSEVSNPVersion(versions, "2021-01-01")
|
||||||
|
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
assert.NoError(err)
|
assert.NoError(err)
|
||||||
assert.Contains(ops, deleteCmd{
|
assert.Contains(ops, deleteCmd{
|
||||||
apiObject: SEVSNPVersionAPI{
|
apiObject: attestationconfigapi.SEVSNPVersionAPI{
|
||||||
Version: "2021-01-01.json",
|
Version: "2021-01-01.json",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
assert.Contains(ops, putCmd{
|
assert.Contains(ops, putCmd{
|
||||||
apiObject: SEVSNPVersionList{list: []string{"2023-01-01.json", "2019-01-01.json"}},
|
apiObject: attestationconfigapi.SEVSNPVersionList{List: []string{"2023-01-01.json", "2019-01-01.json"}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -4,12 +4,7 @@ Copyright (c) Edgeless Systems GmbH
|
|||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*
|
package client
|
||||||
The reporter contains the logic to determine a latest version for SEVSNP based on cached version values observed on CVM instances.
|
|
||||||
Some code in this file (e.g. listing cached files) does not rely on dedicated API objects and instead uses the AWS SDK directly,
|
|
||||||
for no other reason than original development speed.
|
|
||||||
*/
|
|
||||||
package attestationconfigapi
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -23,6 +18,7 @@ import (
|
|||||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||||
"github.com/aws/aws-sdk-go/aws"
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/api/client"
|
"github.com/edgelesssys/constellation/v2/internal/api/client"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||||
)
|
)
|
||||||
@ -34,7 +30,7 @@ const cachedVersionsSubDir = "cached-versions"
|
|||||||
var ErrNoNewerVersion = errors.New("input version is not newer than latest API version")
|
var ErrNoNewerVersion = errors.New("input version is not newer than latest API version")
|
||||||
|
|
||||||
func reportVersionDir(attestation variant.Variant) string {
|
func reportVersionDir(attestation variant.Variant) string {
|
||||||
return path.Join(AttestationURLPath, attestation.String(), cachedVersionsSubDir)
|
return path.Join(attestationconfigapi.AttestationURLPath, attestation.String(), cachedVersionsSubDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadSEVSNPVersionLatest saves the given version to the cache, determines the smallest
|
// UploadSEVSNPVersionLatest saves the given version to the cache, determines the smallest
|
||||||
@ -42,7 +38,7 @@ func reportVersionDir(attestation variant.Variant) string {
|
|||||||
// the latest version in the API if there is an update.
|
// the latest version in the API if there is an update.
|
||||||
// force can be used to bypass the validation logic against the cached versions.
|
// force can be used to bypass the validation logic against the cached versions.
|
||||||
func (c Client) UploadSEVSNPVersionLatest(ctx context.Context, attestation variant.Variant, inputVersion,
|
func (c Client) UploadSEVSNPVersionLatest(ctx context.Context, attestation variant.Variant, inputVersion,
|
||||||
latestAPIVersion SEVSNPVersion, now time.Time, force bool,
|
latestAPIVersion attestationconfigapi.SEVSNPVersion, now time.Time, force bool,
|
||||||
) error {
|
) error {
|
||||||
if err := c.cacheSEVSNPVersion(ctx, attestation, inputVersion, now); err != nil {
|
if err := c.cacheSEVSNPVersion(ctx, attestation, inputVersion, now); err != nil {
|
||||||
return fmt.Errorf("reporting version: %w", err)
|
return fmt.Errorf("reporting version: %w", err)
|
||||||
@ -84,7 +80,7 @@ func (c Client) UploadSEVSNPVersionLatest(ctx context.Context, attestation varia
|
|||||||
}
|
}
|
||||||
|
|
||||||
// cacheSEVSNPVersion uploads the latest observed version numbers of the SEVSNP. This version is used to later report the latest version numbers to the API.
|
// cacheSEVSNPVersion uploads the latest observed version numbers of the SEVSNP. This version is used to later report the latest version numbers to the API.
|
||||||
func (c Client) cacheSEVSNPVersion(ctx context.Context, attestation variant.Variant, version SEVSNPVersion, date time.Time) error {
|
func (c Client) cacheSEVSNPVersion(ctx context.Context, attestation variant.Variant, version attestationconfigapi.SEVSNPVersion, date time.Time) error {
|
||||||
dateStr := date.Format(VersionFormat) + ".json"
|
dateStr := date.Format(VersionFormat) + ".json"
|
||||||
res := putCmd{
|
res := putCmd{
|
||||||
apiObject: reportedSEVSNPVersionAPI{Version: dateStr, variant: attestation, SEVSNPVersion: version},
|
apiObject: reportedSEVSNPVersionAPI{Version: dateStr, variant: attestation, SEVSNPVersion: version},
|
||||||
@ -112,8 +108,8 @@ func (c Client) listCachedVersions(ctx context.Context, attestation variant.Vari
|
|||||||
}
|
}
|
||||||
|
|
||||||
// findMinVersion finds the minimal version of the given version dates among the latest values in the version window size.
|
// findMinVersion finds the minimal version of the given version dates among the latest values in the version window size.
|
||||||
func (c Client) findMinVersion(ctx context.Context, attesation variant.Variant, versionDates []string) (SEVSNPVersion, string, error) {
|
func (c Client) findMinVersion(ctx context.Context, attesation variant.Variant, versionDates []string) (attestationconfigapi.SEVSNPVersion, string, error) {
|
||||||
var minimalVersion *SEVSNPVersion
|
var minimalVersion *attestationconfigapi.SEVSNPVersion
|
||||||
var minimalDate string
|
var minimalDate string
|
||||||
sort.Sort(sort.Reverse(sort.StringSlice(versionDates))) // sort in reverse order to slice the latest versions
|
sort.Sort(sort.Reverse(sort.StringSlice(versionDates))) // sort in reverse order to slice the latest versions
|
||||||
versionDates = versionDates[:c.cacheWindowSize]
|
versionDates = versionDates[:c.cacheWindowSize]
|
||||||
@ -121,7 +117,7 @@ func (c Client) findMinVersion(ctx context.Context, attesation variant.Variant,
|
|||||||
for _, date := range versionDates {
|
for _, date := range versionDates {
|
||||||
obj, err := client.Fetch(ctx, c.s3Client, reportedSEVSNPVersionAPI{Version: date + ".json", variant: attesation})
|
obj, err := client.Fetch(ctx, c.s3Client, reportedSEVSNPVersionAPI{Version: date + ".json", variant: attesation})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return SEVSNPVersion{}, "", fmt.Errorf("get object: %w", err)
|
return attestationconfigapi.SEVSNPVersion{}, "", fmt.Errorf("get object: %w", err)
|
||||||
}
|
}
|
||||||
// Need to set this explicitly as the variant is not part of the marshalled JSON.
|
// Need to set this explicitly as the variant is not part of the marshalled JSON.
|
||||||
obj.variant = attesation
|
obj.variant = attesation
|
||||||
@ -144,7 +140,7 @@ func (c Client) findMinVersion(ctx context.Context, attesation variant.Variant,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isInputNewerThanOtherVersion compares all version fields and returns true if any input field is newer.
|
// isInputNewerThanOtherVersion compares all version fields and returns true if any input field is newer.
|
||||||
func isInputNewerThanOtherVersion(input, other SEVSNPVersion) (bool, error) {
|
func isInputNewerThanOtherVersion(input, other attestationconfigapi.SEVSNPVersion) (bool, error) {
|
||||||
if input == other {
|
if input == other {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
@ -167,7 +163,7 @@ func isInputNewerThanOtherVersion(input, other SEVSNPVersion) (bool, error) {
|
|||||||
type reportedSEVSNPVersionAPI struct {
|
type reportedSEVSNPVersionAPI struct {
|
||||||
Version string `json:"-"`
|
Version string `json:"-"`
|
||||||
variant variant.Variant `json:"-"`
|
variant variant.Variant `json:"-"`
|
||||||
SEVSNPVersion
|
attestationconfigapi.SEVSNPVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
@ -2,17 +2,18 @@
|
|||||||
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 attestationconfigapi
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIsInputNewerThanLatestAPI(t *testing.T) {
|
func TestIsInputNewerThanLatestAPI(t *testing.T) {
|
||||||
newTestCfg := func() SEVSNPVersion {
|
newTestCfg := func() attestationconfigapi.SEVSNPVersion {
|
||||||
return SEVSNPVersion{
|
return attestationconfigapi.SEVSNPVersion{
|
||||||
Microcode: 93,
|
Microcode: 93,
|
||||||
TEE: 0,
|
TEE: 0,
|
||||||
SNP: 6,
|
SNP: 6,
|
||||||
@ -21,13 +22,13 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
testCases := map[string]struct {
|
testCases := map[string]struct {
|
||||||
latest SEVSNPVersion
|
latest attestationconfigapi.SEVSNPVersion
|
||||||
input SEVSNPVersion
|
input attestationconfigapi.SEVSNPVersion
|
||||||
expect bool
|
expect bool
|
||||||
errMsg string
|
errMsg string
|
||||||
}{
|
}{
|
||||||
"input is older than latest": {
|
"input is older than latest": {
|
||||||
input: func(c SEVSNPVersion) SEVSNPVersion {
|
input: func(c attestationconfigapi.SEVSNPVersion) attestationconfigapi.SEVSNPVersion {
|
||||||
c.Microcode--
|
c.Microcode--
|
||||||
return c
|
return c
|
||||||
}(newTestCfg()),
|
}(newTestCfg()),
|
||||||
@ -36,7 +37,7 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
|
|||||||
errMsg: "input Microcode version: 92 is older than latest API version: 93",
|
errMsg: "input Microcode version: 92 is older than latest API version: 93",
|
||||||
},
|
},
|
||||||
"input has greater and smaller version field than latest": {
|
"input has greater and smaller version field than latest": {
|
||||||
input: func(c SEVSNPVersion) SEVSNPVersion {
|
input: func(c attestationconfigapi.SEVSNPVersion) attestationconfigapi.SEVSNPVersion {
|
||||||
c.Microcode++
|
c.Microcode++
|
||||||
c.Bootloader--
|
c.Bootloader--
|
||||||
return c
|
return c
|
||||||
@ -46,7 +47,7 @@ func TestIsInputNewerThanLatestAPI(t *testing.T) {
|
|||||||
errMsg: "input Bootloader version: 1 is older than latest API version: 2",
|
errMsg: "input Bootloader version: 1 is older than latest API version: 2",
|
||||||
},
|
},
|
||||||
"input is newer than latest": {
|
"input is newer than latest": {
|
||||||
input: func(c SEVSNPVersion) SEVSNPVersion {
|
input: func(c attestationconfigapi.SEVSNPVersion) attestationconfigapi.SEVSNPVersion {
|
||||||
c.TEE++
|
c.TEE++
|
||||||
return c
|
return c
|
||||||
}(newTestCfg()),
|
}(newTestCfg()),
|
@ -16,6 +16,7 @@ import (
|
|||||||
"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/api/attestationconfigapi"
|
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli/client"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/logger"
|
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||||
@ -62,7 +63,7 @@ func runDelete(cmd *cobra.Command, args []string) (retErr error) {
|
|||||||
Region: deleteCfg.region,
|
Region: deleteCfg.region,
|
||||||
DistributionID: deleteCfg.distribution,
|
DistributionID: deleteCfg.distribution,
|
||||||
}
|
}
|
||||||
client, clientClose, err := attestationconfigapi.NewClient(cmd.Context(), cfg,
|
client, clientClose, err := client.New(cmd.Context(), cfg,
|
||||||
[]byte(cosignPwd), []byte(privateKey), false, 1, log)
|
[]byte(cosignPwd), []byte(privateKey), false, 1, log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("create attestation client: %w", err)
|
return fmt.Errorf("create attestation client: %w", err)
|
||||||
@ -170,7 +171,7 @@ func newDeleteConfig(cmd *cobra.Command, args [3]string) (deleteConfig, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteEntry(ctx context.Context, attvar variant.Variant, client *attestationconfigapi.Client, cfg deleteConfig) error {
|
func deleteEntry(ctx context.Context, attvar variant.Variant, client *client.Client, cfg deleteConfig) error {
|
||||||
if cfg.kind != snpReport {
|
if cfg.kind != snpReport {
|
||||||
return fmt.Errorf("kind %s not supported", cfg.kind)
|
return fmt.Errorf("kind %s not supported", cfg.kind)
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi/cli/client"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/file"
|
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||||
@ -68,18 +69,15 @@ func runUpload(cmd *cobra.Command, args []string) (retErr error) {
|
|||||||
return fmt.Errorf("parsing cli flags: %w", err)
|
return fmt.Errorf("parsing cli flags: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client, clientClose, err := attestationconfigapi.NewClient(
|
client, clientClose, err := client.New(ctx,
|
||||||
ctx,
|
|
||||||
staticupload.Config{
|
staticupload.Config{
|
||||||
Bucket: uploadCfg.bucket,
|
Bucket: uploadCfg.bucket,
|
||||||
Region: uploadCfg.region,
|
Region: uploadCfg.region,
|
||||||
DistributionID: uploadCfg.distribution,
|
DistributionID: uploadCfg.distribution,
|
||||||
},
|
},
|
||||||
[]byte(cosignPwd),
|
[]byte(cosignPwd), []byte(privateKey),
|
||||||
[]byte(privateKey),
|
false, uploadCfg.cacheWindowSize, log,
|
||||||
false,
|
)
|
||||||
uploadCfg.cacheWindowSize,
|
|
||||||
log)
|
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
err := clientClose(cmd.Context())
|
err := clientClose(cmd.Context())
|
||||||
@ -109,7 +107,7 @@ func runUpload(cmd *cobra.Command, args []string) (retErr error) {
|
|||||||
|
|
||||||
func uploadReport(ctx context.Context,
|
func uploadReport(ctx context.Context,
|
||||||
attestation variant.Variant,
|
attestation variant.Variant,
|
||||||
client *attestationconfigapi.Client,
|
apiClient *client.Client,
|
||||||
cfg uploadConfig,
|
cfg uploadConfig,
|
||||||
fs file.Handler,
|
fs file.Handler,
|
||||||
log *slog.Logger,
|
log *slog.Logger,
|
||||||
@ -137,8 +135,8 @@ func uploadReport(ctx context.Context,
|
|||||||
}
|
}
|
||||||
|
|
||||||
latestAPIVersion := latestAPIVersionAPI.SEVSNPVersion
|
latestAPIVersion := latestAPIVersionAPI.SEVSNPVersion
|
||||||
if err := client.UploadSEVSNPVersionLatest(ctx, attestation, inputVersion, latestAPIVersion, cfg.uploadDate, cfg.force); err != nil {
|
if err := apiClient.UploadSEVSNPVersionLatest(ctx, attestation, inputVersion, latestAPIVersion, cfg.uploadDate, cfg.force); err != nil {
|
||||||
if errors.Is(err, attestationconfigapi.ErrNoNewerVersion) {
|
if errors.Is(err, client.ErrNoNewerVersion) {
|
||||||
log.Info(fmt.Sprintf("Input version: %+v is not newer than latest API version: %+v", inputVersion, latestAPIVersion))
|
log.Info(fmt.Sprintf("Input version: %+v is not newer than latest API version: %+v", inputVersion, latestAPIVersion))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -178,7 +176,7 @@ func newConfig(cmd *cobra.Command, args [3]string) (uploadConfig, error) {
|
|||||||
}
|
}
|
||||||
uploadDate := time.Now()
|
uploadDate := time.Now()
|
||||||
if dateStr != "" {
|
if dateStr != "" {
|
||||||
uploadDate, err = time.Parse(attestationconfigapi.VersionFormat, dateStr)
|
uploadDate, err = time.Parse(client.VersionFormat, dateStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return uploadConfig{}, fmt.Errorf("parsing date: %w", err)
|
return uploadConfig{}, fmt.Errorf("parsing date: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -39,13 +39,14 @@ const (
|
|||||||
// unknown is the default objectKind and does nothing.
|
// unknown is the default objectKind and does nothing.
|
||||||
unknown objectKind = "unknown-kind"
|
unknown objectKind = "unknown-kind"
|
||||||
snpReport objectKind = "snp-report"
|
snpReport objectKind = "snp-report"
|
||||||
|
tdxReport objectKind = "tdx-report"
|
||||||
guestFirmware objectKind = "guest-firmware"
|
guestFirmware objectKind = "guest-firmware"
|
||||||
)
|
)
|
||||||
|
|
||||||
func kindFromString(s string) objectKind {
|
func kindFromString(s string) objectKind {
|
||||||
lower := strings.ToLower(s)
|
lower := strings.ToLower(s)
|
||||||
switch objectKind(lower) {
|
switch objectKind(lower) {
|
||||||
case snpReport, guestFirmware:
|
case snpReport, guestFirmware, tdxReport:
|
||||||
return objectKind(lower)
|
return objectKind(lower)
|
||||||
default:
|
default:
|
||||||
return unknown
|
return unknown
|
||||||
|
@ -74,7 +74,7 @@ func (f *fetcher) FetchSEVSNPVersionList(ctx context.Context, list SEVSNPVersion
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Need to set this explicitly as the variant is not part of the marshalled JSON.
|
// Need to set this explicitly as the variant is not part of the marshalled JSON.
|
||||||
fetchedList.variant = list.variant
|
fetchedList.Variant = list.Variant
|
||||||
|
|
||||||
return fetchedList, nil
|
return fetchedList, nil
|
||||||
}
|
}
|
||||||
@ -94,13 +94,13 @@ func (f *fetcher) FetchSEVSNPVersion(ctx context.Context, version SEVSNPVersionA
|
|||||||
|
|
||||||
// FetchSEVSNPVersionLatest returns the latest versions of the given type.
|
// FetchSEVSNPVersionLatest returns the latest versions of the given type.
|
||||||
func (f *fetcher) FetchSEVSNPVersionLatest(ctx context.Context, attesation variant.Variant) (res SEVSNPVersionAPI, err error) {
|
func (f *fetcher) FetchSEVSNPVersionLatest(ctx context.Context, attesation variant.Variant) (res SEVSNPVersionAPI, err error) {
|
||||||
list, err := f.FetchSEVSNPVersionList(ctx, SEVSNPVersionList{variant: attesation})
|
list, err := f.FetchSEVSNPVersionList(ctx, SEVSNPVersionList{Variant: attesation})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, ErrNoVersionsFound
|
return res, ErrNoVersionsFound
|
||||||
}
|
}
|
||||||
|
|
||||||
getVersionRequest := SEVSNPVersionAPI{
|
getVersionRequest := SEVSNPVersionAPI{
|
||||||
Version: list.List()[0], // latest version is first in list
|
Version: list.List[0], // latest version is first in list
|
||||||
Variant: attesation,
|
Variant: attesation,
|
||||||
}
|
}
|
||||||
res, err = f.FetchSEVSNPVersion(ctx, getVersionRequest)
|
res, err = f.FetchSEVSNPVersion(ctx, getVersionRequest)
|
||||||
|
@ -64,26 +64,23 @@ func (i SEVSNPVersionAPI) Validate() error {
|
|||||||
// Once we switch to v2 of the API we could embed the variant in the object and remove some code from fetcher & client.
|
// Once we switch to v2 of the API we could embed the variant in the object and remove some code from fetcher & client.
|
||||||
// That would remove the possibility of some fetcher/client code forgetting to set the variant.
|
// That would remove the possibility of some fetcher/client code forgetting to set the variant.
|
||||||
type SEVSNPVersionList struct {
|
type SEVSNPVersionList struct {
|
||||||
variant variant.Variant
|
Variant variant.Variant
|
||||||
list []string
|
List []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// MarshalJSON marshals the i's list property to JSON.
|
// MarshalJSON marshals the i's list property to JSON.
|
||||||
func (i SEVSNPVersionList) MarshalJSON() ([]byte, error) {
|
func (i SEVSNPVersionList) MarshalJSON() ([]byte, error) {
|
||||||
return json.Marshal(i.list)
|
return json.Marshal(i.List)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON unmarshals a list of strings into i's list property.
|
// UnmarshalJSON unmarshals a list of strings into i's list property.
|
||||||
func (i *SEVSNPVersionList) UnmarshalJSON(data []byte) error {
|
func (i *SEVSNPVersionList) UnmarshalJSON(data []byte) error {
|
||||||
return json.Unmarshal(data, &i.list)
|
return json.Unmarshal(data, &i.List)
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns i's list property.
|
|
||||||
func (i SEVSNPVersionList) List() []string { return i.list }
|
|
||||||
|
|
||||||
// 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 SEVSNPVersionList) JSONPath() string {
|
func (i SEVSNPVersionList) JSONPath() string {
|
||||||
return path.Join(AttestationURLPath, i.variant.String(), "list")
|
return path.Join(AttestationURLPath, i.Variant.String(), "list")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateRequest is a NoOp as there is no input.
|
// ValidateRequest is a NoOp as there is no input.
|
||||||
@ -93,20 +90,20 @@ func (i SEVSNPVersionList) ValidateRequest() error {
|
|||||||
|
|
||||||
// SortReverse sorts the list of versions in reverse order.
|
// SortReverse sorts the list of versions in reverse order.
|
||||||
func (i *SEVSNPVersionList) SortReverse() {
|
func (i *SEVSNPVersionList) SortReverse() {
|
||||||
sort.Sort(sort.Reverse(sort.StringSlice(i.list)))
|
sort.Sort(sort.Reverse(sort.StringSlice(i.List)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// addVersion adds new to i's list and sorts the element in descending order.
|
// AddVersion adds new to i's list and sorts the element in descending order.
|
||||||
func (i *SEVSNPVersionList) addVersion(new string) {
|
func (i *SEVSNPVersionList) AddVersion(new string) {
|
||||||
i.list = append(i.list, new)
|
i.List = append(i.List, new)
|
||||||
i.list = variant.RemoveDuplicate(i.list)
|
i.List = variant.RemoveDuplicate(i.List)
|
||||||
|
|
||||||
i.SortReverse()
|
i.SortReverse()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the response.
|
// Validate validates the response.
|
||||||
func (i SEVSNPVersionList) Validate() error {
|
func (i SEVSNPVersionList) Validate() error {
|
||||||
if len(i.list) < 1 {
|
if len(i.List) < 1 {
|
||||||
return fmt.Errorf("no versions found in /list")
|
return fmt.Errorf("no versions found in /list")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -21,16 +21,16 @@ func TestSEVSNPVersionListMarshalUnmarshalJSON(t *testing.T) {
|
|||||||
wantDiff bool
|
wantDiff bool
|
||||||
}{
|
}{
|
||||||
"success": {
|
"success": {
|
||||||
input: SEVSNPVersionList{list: []string{"v1", "v2"}},
|
input: SEVSNPVersionList{List: []string{"v1", "v2"}},
|
||||||
output: SEVSNPVersionList{list: []string{"v1", "v2"}},
|
output: SEVSNPVersionList{List: []string{"v1", "v2"}},
|
||||||
},
|
},
|
||||||
"variant is lost": {
|
"variant is lost": {
|
||||||
input: SEVSNPVersionList{list: []string{"v1", "v2"}, variant: variant.AzureSEVSNP{}},
|
input: SEVSNPVersionList{List: []string{"v1", "v2"}, Variant: variant.AzureSEVSNP{}},
|
||||||
output: SEVSNPVersionList{list: []string{"v1", "v2"}},
|
output: SEVSNPVersionList{List: []string{"v1", "v2"}},
|
||||||
},
|
},
|
||||||
"wrong order": {
|
"wrong order": {
|
||||||
input: SEVSNPVersionList{list: []string{"v1", "v2"}},
|
input: SEVSNPVersionList{List: []string{"v1", "v2"}},
|
||||||
output: SEVSNPVersionList{list: []string{"v2", "v1"}},
|
output: SEVSNPVersionList{List: []string{"v2", "v1"}},
|
||||||
wantDiff: true,
|
wantDiff: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -68,10 +68,10 @@ func TestSEVSNPVersionListAddVersion(t *testing.T) {
|
|||||||
|
|
||||||
for name, tc := range tests {
|
for name, tc := range tests {
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
v := SEVSNPVersionList{list: tc.versions}
|
v := SEVSNPVersionList{List: tc.versions}
|
||||||
v.addVersion(tc.new)
|
v.AddVersion(tc.new)
|
||||||
|
|
||||||
assert.Equal(t, tc.expected, v.list)
|
assert.Equal(t, tc.expected, v.List)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user