mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-01-13 08:29:38 -05:00
versionsapi: new implementation
Signed-off-by: Paul Meyer <49727155+katexochen@users.noreply.github.com>
This commit is contained in:
parent
f43b653231
commit
3f00f89d55
114
internal/versionsapi/imageinfo.go
Normal file
114
internal/versionsapi/imageinfo.go
Normal file
@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package versionsapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"go.uber.org/multierr"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// ImageInfo contains information about the OS images for a specific version.
|
||||
type ImageInfo struct {
|
||||
// Ref is the reference name of the image.
|
||||
Ref string `json:"ref,omitempty"`
|
||||
// Stream is the stream name of the image.
|
||||
Stream string `json:"stream,omitempty"`
|
||||
// Version is the version of the image.
|
||||
Version string `json:"version,omitempty"`
|
||||
// AWS is a map of AWS regions to AMI IDs.
|
||||
AWS map[string]string `json:"aws,omitempty"`
|
||||
// Azure is a map of image types to Azure image IDs.
|
||||
Azure map[string]string `json:"azure,omitempty"`
|
||||
// GCP is a map of image types to GCP image IDs.
|
||||
GCP map[string]string `json:"gcp,omitempty"`
|
||||
// QEMU is a map of image types to QEMU image URLs.
|
||||
QEMU map[string]string `json:"qemu,omitempty"`
|
||||
}
|
||||
|
||||
// JSONPath returns the S3 JSON path for this object.
|
||||
func (i ImageInfo) JSONPath() string {
|
||||
return path.Join(
|
||||
constants.CDNAPIPrefix,
|
||||
"ref", i.Ref,
|
||||
"stream", i.Stream,
|
||||
"image", i.Version,
|
||||
"info.json",
|
||||
)
|
||||
}
|
||||
|
||||
// URL returns the URL to the JSON file for this object.
|
||||
func (i ImageInfo) URL() (string, error) {
|
||||
url, err := url.Parse(constants.CDNRepositoryURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parsing CDN URL: %w", err)
|
||||
}
|
||||
url.Path = i.JSONPath()
|
||||
return url.String(), nil
|
||||
}
|
||||
|
||||
// ValidateRequest validates the request parameters of the list.
|
||||
// The provider specific maps must be empty.
|
||||
func (i ImageInfo) ValidateRequest() error {
|
||||
var retErr error
|
||||
if err := ValidateRef(i.Ref); err != nil {
|
||||
retErr = multierr.Append(retErr, err)
|
||||
}
|
||||
if err := ValidateStream(i.Ref, i.Stream); err != nil {
|
||||
retErr = multierr.Append(retErr, err)
|
||||
}
|
||||
if !semver.IsValid(i.Version) {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("version %q is not a valid semver", i.Version))
|
||||
}
|
||||
if len(i.AWS) != 0 {
|
||||
retErr = multierr.Append(retErr, errors.New("AWS map must be empty for request"))
|
||||
}
|
||||
if len(i.Azure) != 0 {
|
||||
retErr = multierr.Append(retErr, errors.New("Azure map must be empty for request"))
|
||||
}
|
||||
if len(i.GCP) != 0 {
|
||||
retErr = multierr.Append(retErr, errors.New("GCP map must be empty for request"))
|
||||
}
|
||||
if len(i.QEMU) != 0 {
|
||||
retErr = multierr.Append(retErr, errors.New("QEMU map must be empty for request"))
|
||||
}
|
||||
|
||||
return retErr
|
||||
}
|
||||
|
||||
// Validate checks if the image info is valid.
|
||||
func (i ImageInfo) Validate() error {
|
||||
var retErr error
|
||||
if err := ValidateRef(i.Ref); err != nil {
|
||||
retErr = multierr.Append(retErr, err)
|
||||
}
|
||||
if err := ValidateStream(i.Ref, i.Stream); err != nil {
|
||||
retErr = multierr.Append(retErr, err)
|
||||
}
|
||||
if !semver.IsValid(i.Version) {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("version %q is not a valid semver", i.Version))
|
||||
}
|
||||
if len(i.AWS) == 0 {
|
||||
retErr = multierr.Append(retErr, errors.New("AWS map must not be empty"))
|
||||
}
|
||||
if len(i.Azure) == 0 {
|
||||
retErr = multierr.Append(retErr, errors.New("Azure map must not be empty"))
|
||||
}
|
||||
if len(i.GCP) == 0 {
|
||||
retErr = multierr.Append(retErr, errors.New("GCP map must not be empty"))
|
||||
}
|
||||
if len(i.QEMU) == 0 {
|
||||
retErr = multierr.Append(retErr, errors.New("QEMU map must not be empty"))
|
||||
}
|
||||
|
||||
return retErr
|
||||
}
|
298
internal/versionsapi/imageinfo_test.go
Normal file
298
internal/versionsapi/imageinfo_test.go
Normal file
@ -0,0 +1,298 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package versionsapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestImageInfoJSONPath(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
info ImageInfo
|
||||
wantPath string
|
||||
}{
|
||||
"image info": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Version: "v1.0.0",
|
||||
},
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/test-ref/stream/nightly/image/v1.0.0/info.json",
|
||||
},
|
||||
"image info release": {
|
||||
info: ImageInfo{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v1.0.0",
|
||||
},
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/-/stream/stable/image/v1.0.0/info.json",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.wantPath, tc.info.JSONPath())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageInfoURL(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
info ImageInfo
|
||||
wantURL string
|
||||
wantPath string
|
||||
}{
|
||||
"image info": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Version: "v1.0.0",
|
||||
},
|
||||
wantURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/test-ref/stream/nightly/image/v1.0.0/info.json",
|
||||
},
|
||||
"image info release": {
|
||||
info: ImageInfo{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v1.0.0",
|
||||
},
|
||||
wantURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/-/stream/stable/image/v1.0.0/info.json",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
url, err := tc.info.URL()
|
||||
assert.NoError(err)
|
||||
assert.Equal(tc.wantURL, url)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageInfoValidate(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
info ImageInfo
|
||||
wantErr bool
|
||||
}{
|
||||
"valid image info": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Version: "v1.0.0",
|
||||
AWS: map[string]string{"key": "value", "key2": "value2"},
|
||||
GCP: map[string]string{"key": "value", "key2": "value2"},
|
||||
Azure: map[string]string{"key": "value", "key2": "value2"},
|
||||
QEMU: map[string]string{"key": "value", "key2": "value2"},
|
||||
},
|
||||
},
|
||||
"invalid ref": {
|
||||
info: ImageInfo{
|
||||
Ref: "",
|
||||
Stream: "nightly",
|
||||
Version: "v1.0.0",
|
||||
AWS: map[string]string{"key": "value", "key2": "value2"},
|
||||
GCP: map[string]string{"key": "value", "key2": "value2"},
|
||||
Azure: map[string]string{"key": "value", "key2": "value2"},
|
||||
QEMU: map[string]string{"key": "value", "key2": "value2"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid stream": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "",
|
||||
Version: "v1.0.0",
|
||||
AWS: map[string]string{"key": "value", "key2": "value2"},
|
||||
GCP: map[string]string{"key": "value", "key2": "value2"},
|
||||
Azure: map[string]string{"key": "value", "key2": "value2"},
|
||||
QEMU: map[string]string{"key": "value", "key2": "value2"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid version": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Version: "",
|
||||
AWS: map[string]string{"key": "value", "key2": "value2"},
|
||||
GCP: map[string]string{"key": "value", "key2": "value2"},
|
||||
Azure: map[string]string{"key": "value", "key2": "value2"},
|
||||
QEMU: map[string]string{"key": "value", "key2": "value2"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid aws": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Version: "v1.0.0",
|
||||
GCP: map[string]string{"key": "value", "key2": "value2"},
|
||||
Azure: map[string]string{"key": "value", "key2": "value2"},
|
||||
QEMU: map[string]string{"key": "value", "key2": "value2"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid gcp": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Version: "v1.0.0",
|
||||
AWS: map[string]string{"key": "value", "key2": "value2"},
|
||||
Azure: map[string]string{"key": "value", "key2": "value2"},
|
||||
QEMU: map[string]string{"key": "value", "key2": "value2"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid azure": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Version: "v1.0.0",
|
||||
AWS: map[string]string{"key": "value", "key2": "value2"},
|
||||
GCP: map[string]string{"key": "value", "key2": "value2"},
|
||||
QEMU: map[string]string{"key": "value", "key2": "value2"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid qemu": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Version: "v1.0.0",
|
||||
AWS: map[string]string{"key": "value", "key2": "value2"},
|
||||
GCP: map[string]string{"key": "value", "key2": "value2"},
|
||||
Azure: map[string]string{"key": "value", "key2": "value2"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"multiple errors": {
|
||||
info: ImageInfo{
|
||||
Ref: "",
|
||||
Stream: "",
|
||||
Version: "",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
err := tc.info.Validate()
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageInfoValidateRequest(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
info ImageInfo
|
||||
wantErr bool
|
||||
}{
|
||||
"valid image info": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Version: "v1.0.0",
|
||||
},
|
||||
},
|
||||
"invalid ref": {
|
||||
info: ImageInfo{
|
||||
Ref: "",
|
||||
Stream: "nightly",
|
||||
Version: "v1.0.0",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid stream": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "",
|
||||
Version: "v1.0.0",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid version": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Version: "",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid gcp": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Version: "v1.0.0",
|
||||
GCP: map[string]string{"key": "value", "key2": "value2"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid azure": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Version: "v1.0.0",
|
||||
Azure: map[string]string{"key": "value", "key2": "value2"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid aws": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Version: "v1.0.0",
|
||||
AWS: map[string]string{"key": "value", "key2": "value2"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid qemu": {
|
||||
info: ImageInfo{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Version: "v1.0.0",
|
||||
QEMU: map[string]string{"key": "value", "key2": "value2"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"multiple errors": {
|
||||
info: ImageInfo{
|
||||
Ref: "",
|
||||
Stream: "",
|
||||
Version: "",
|
||||
AWS: map[string]string{"key": "value", "key2": "value2"},
|
||||
GCP: map[string]string{"key": "value", "key2": "value2"},
|
||||
Azure: map[string]string{"key": "value", "key2": "value2"},
|
||||
QEMU: map[string]string{"key": "value", "key2": "value2"},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
err := tc.info.ValidateRequest()
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
93
internal/versionsapi/latest.go
Normal file
93
internal/versionsapi/latest.go
Normal file
@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package versionsapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"go.uber.org/multierr"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// Latest is the latest version of a kind of resource.
|
||||
type Latest struct {
|
||||
// Ref is the branch name this latest version belongs to.
|
||||
Ref string `json:"ref,omitempty"`
|
||||
// Stream is stream name this latest version belongs to.
|
||||
Stream string `json:"stream,omitempty"`
|
||||
// Kind is the kind of resource this latest version is for.
|
||||
Kind VersionKind `json:"kind,omitempty"`
|
||||
// Version is the latest version for this ref, stream and kind.
|
||||
Version string `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
// JSONPath returns the S3 JSON path for this object.
|
||||
func (l Latest) JSONPath() string {
|
||||
return path.Join(
|
||||
constants.CDNAPIPrefix,
|
||||
"ref", l.Ref,
|
||||
"stream", l.Stream,
|
||||
"versions",
|
||||
"latest",
|
||||
l.Kind.String()+".json",
|
||||
)
|
||||
}
|
||||
|
||||
// URL returns the URL for this object.
|
||||
func (l Latest) URL() (string, error) {
|
||||
url, err := url.Parse(constants.CDNRepositoryURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parsing CDN URL: %w", err)
|
||||
}
|
||||
url.Path = l.JSONPath()
|
||||
return url.String(), nil
|
||||
}
|
||||
|
||||
// Validate checks if this latest version is valid.
|
||||
func (l Latest) Validate() error {
|
||||
var retErr error
|
||||
if err := ValidateRef(l.Ref); err != nil {
|
||||
retErr = multierr.Append(retErr, err)
|
||||
}
|
||||
if err := ValidateStream(l.Ref, l.Stream); err != nil {
|
||||
retErr = multierr.Append(retErr, err)
|
||||
}
|
||||
if l.Kind == VersionKindUnknown {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("version of kind %q is not supported", l.Kind))
|
||||
}
|
||||
if !semver.IsValid(l.Version) {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("version %q is not a valid semver", l.Version))
|
||||
}
|
||||
|
||||
return retErr
|
||||
}
|
||||
|
||||
// ValidateRequest checks if this latest version beside values that are fetched.
|
||||
func (l Latest) ValidateRequest() error {
|
||||
var retErr error
|
||||
if err := ValidateRef(l.Ref); err != nil {
|
||||
retErr = multierr.Append(retErr, err)
|
||||
}
|
||||
if err := ValidateStream(l.Ref, l.Stream); err != nil {
|
||||
retErr = multierr.Append(retErr, err)
|
||||
}
|
||||
if l.Kind == VersionKindUnknown {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("version of kind %q is not supported", l.Kind))
|
||||
}
|
||||
if l.Version != "" {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("version %q must be empty for request", l.Version))
|
||||
}
|
||||
return retErr
|
||||
}
|
||||
|
||||
// ShortPath returns the short path of the latest version.
|
||||
func (l Latest) ShortPath() string {
|
||||
return shortPath(l.Ref, l.Stream, l.Version)
|
||||
}
|
212
internal/versionsapi/latest_test.go
Normal file
212
internal/versionsapi/latest_test.go
Normal file
@ -0,0 +1,212 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package versionsapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLatestJSONPath(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
list Latest
|
||||
wantPath string
|
||||
}{
|
||||
"latest list": {
|
||||
list: Latest{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/test-ref/stream/nightly/versions/latest/image.json",
|
||||
},
|
||||
"latest list release": {
|
||||
list: Latest{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/-/stream/stable/versions/latest/image.json",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.wantPath, tc.list.JSONPath())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLatestURL(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
list Latest
|
||||
wantURL string
|
||||
wantPath string
|
||||
}{
|
||||
"latest list": {
|
||||
list: Latest{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/test-ref/stream/nightly/versions/latest/image.json",
|
||||
},
|
||||
"latest list release": {
|
||||
list: Latest{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/-/stream/stable/versions/latest/image.json",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
url, err := tc.list.URL()
|
||||
assert.NoError(err)
|
||||
assert.Equal(tc.wantURL, url)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLatestValidate(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
list Latest
|
||||
wantErr bool
|
||||
}{
|
||||
"valid": {
|
||||
list: Latest{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Kind: VersionKindImage,
|
||||
Version: "v1.0.0",
|
||||
},
|
||||
},
|
||||
"invalid ref": {
|
||||
list: Latest{
|
||||
Ref: "",
|
||||
Stream: "nightly",
|
||||
Kind: VersionKindImage,
|
||||
Version: "v1.0.0",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid stream": {
|
||||
list: Latest{
|
||||
Ref: "test-ref",
|
||||
Stream: "invalid-stream",
|
||||
Kind: VersionKindImage,
|
||||
Version: "v1.0.0",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid kind": {
|
||||
list: Latest{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Kind: VersionKindUnknown,
|
||||
Version: "v1.0.0",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid version": {
|
||||
list: Latest{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Kind: VersionKindImage,
|
||||
Version: "",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if tc.wantErr {
|
||||
assert.Error(t, tc.list.Validate())
|
||||
} else {
|
||||
assert.NoError(t, tc.list.Validate())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLatestValidateRequest(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
list Latest
|
||||
wantErr bool
|
||||
}{
|
||||
"valid": {
|
||||
list: Latest{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
},
|
||||
"invalid ref": {
|
||||
list: Latest{
|
||||
Ref: "",
|
||||
Stream: "nightly",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid stream": {
|
||||
list: Latest{
|
||||
Ref: "test-ref",
|
||||
Stream: "invalid-stream",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid kind": {
|
||||
list: Latest{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Kind: VersionKindUnknown,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"version not empty": {
|
||||
list: Latest{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Kind: VersionKindImage,
|
||||
Version: "v1.1.1",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if tc.wantErr {
|
||||
assert.Error(t, tc.list.ValidateRequest())
|
||||
} else {
|
||||
assert.NoError(t, tc.list.ValidateRequest())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLatestShortPath(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
latest := Latest{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Kind: VersionKindImage,
|
||||
Version: "v1.0.0",
|
||||
}
|
||||
|
||||
assert.Equal(shortPath(latest.Ref, latest.Stream, latest.Version), latest.ShortPath())
|
||||
}
|
173
internal/versionsapi/list.go
Normal file
173
internal/versionsapi/list.go
Normal file
@ -0,0 +1,173 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package versionsapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"go.uber.org/multierr"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// List represents a list of versions for a kind of resource.
|
||||
// It has a granularity of either "major" or "minor".
|
||||
//
|
||||
// For example, a List with granularity "major" could contain
|
||||
// the base version "v1" and a list of minor versions "v1.0", "v1.1", "v1.2" etc.
|
||||
// A List with granularity "minor" could contain the base version
|
||||
// "v1.0" and a list of patch versions "v1.0.0", "v1.0.1", "v1.0.2" etc.
|
||||
type List struct {
|
||||
// Ref is the branch name the list belongs to.
|
||||
Ref string `json:"ref,omitempty"`
|
||||
// Stream is the update stream of the list.
|
||||
Stream string `json:"stream,omitempty"`
|
||||
// Granularity is the granularity of the base version of this list.
|
||||
// It can be either "major" or "minor".
|
||||
Granularity Granularity `json:"granularity,omitempty"`
|
||||
// Base is the base version of the list.
|
||||
// Every version in the list is a finer-grained version of this base version.
|
||||
Base string `json:"base,omitempty"`
|
||||
// Kind is the kind of resource this list is for.
|
||||
Kind VersionKind `json:"kind,omitempty"`
|
||||
// Versions is a list of all versions in this list.
|
||||
Versions []string `json:"versions,omitempty"`
|
||||
}
|
||||
|
||||
// JSONPath returns the S3 JSON path for this object.
|
||||
func (l List) JSONPath() string {
|
||||
return path.Join(
|
||||
constants.CDNAPIPrefix,
|
||||
"ref", l.Ref,
|
||||
"stream", l.Stream,
|
||||
"versions", l.Granularity.String(), l.Base,
|
||||
l.Kind.String()+".json",
|
||||
)
|
||||
}
|
||||
|
||||
// URL returns the URL for this object.
|
||||
func (l List) URL() (string, error) {
|
||||
url, err := url.Parse(constants.CDNRepositoryURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parsing CDN URL: %w", err)
|
||||
}
|
||||
url.Path = l.JSONPath()
|
||||
return url.String(), nil
|
||||
}
|
||||
|
||||
// ValidateRequest validates the request parameters of the list.
|
||||
// The versions field must be empty.
|
||||
func (l List) ValidateRequest() error {
|
||||
var retErr error
|
||||
if err := ValidateRef(l.Ref); err != nil {
|
||||
retErr = multierr.Append(retErr, err)
|
||||
}
|
||||
if err := ValidateStream(l.Ref, l.Stream); err != nil {
|
||||
retErr = multierr.Append(retErr, err)
|
||||
}
|
||||
if l.Granularity != GranularityMajor && l.Granularity != GranularityMinor {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("granularity %q is not supported", l.Granularity))
|
||||
}
|
||||
if l.Kind != VersionKindImage {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("kind %q is not supported", l.Kind))
|
||||
}
|
||||
if !semver.IsValid(l.Base) {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("base version %q is not a valid semantic version", l.Base))
|
||||
}
|
||||
var normalizeFunc func(string) string
|
||||
switch l.Granularity {
|
||||
case GranularityMajor:
|
||||
normalizeFunc = semver.Major
|
||||
case GranularityMinor:
|
||||
normalizeFunc = semver.MajorMinor
|
||||
default:
|
||||
normalizeFunc = func(s string) string { return s }
|
||||
}
|
||||
if normalizeFunc(l.Base) != l.Base {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("base version %q does not match granularity %q", l.Base, l.Granularity))
|
||||
}
|
||||
if len(l.Versions) != 0 {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("versions must be empty for request"))
|
||||
}
|
||||
return retErr
|
||||
}
|
||||
|
||||
// Validate checks if the list is valid.
|
||||
// This performs the following checks:
|
||||
// - The ref is set.
|
||||
// - The stream is supported.
|
||||
// - The granularity is "major" or "minor".
|
||||
// - The kind is supported.
|
||||
// - The base version is a valid semantic version that matches the granularity.
|
||||
// - All versions in the list are valid semantic versions that are finer-grained than the base version.
|
||||
func (l List) Validate() error {
|
||||
var retErr error
|
||||
if err := ValidateRef(l.Ref); err != nil {
|
||||
retErr = multierr.Append(retErr, err)
|
||||
}
|
||||
if err := ValidateStream(l.Ref, l.Stream); err != nil {
|
||||
retErr = multierr.Append(retErr, err)
|
||||
}
|
||||
if l.Granularity != GranularityMajor && l.Granularity != GranularityMinor {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("granularity %q is not supported", l.Granularity))
|
||||
}
|
||||
if l.Kind != VersionKindImage {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("kind %q is not supported", l.Kind))
|
||||
}
|
||||
if !semver.IsValid(l.Base) {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("base version %q is not a valid semantic version", l.Base))
|
||||
}
|
||||
var normalizeFunc func(string) string
|
||||
switch l.Granularity {
|
||||
case GranularityMajor:
|
||||
normalizeFunc = semver.Major
|
||||
case GranularityMinor:
|
||||
normalizeFunc = semver.MajorMinor
|
||||
default:
|
||||
normalizeFunc = func(s string) string { return s }
|
||||
}
|
||||
if normalizeFunc(l.Base) != l.Base {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("base version %q does not match granularity %q", l.Base, l.Granularity))
|
||||
}
|
||||
for _, ver := range l.Versions {
|
||||
if !semver.IsValid(ver) {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("version %q is not a valid semantic version", ver))
|
||||
}
|
||||
if normalizeFunc(ver) != l.Base || normalizeFunc(ver) == ver {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("version %q is not finer-grained than base version %q", ver, l.Base))
|
||||
}
|
||||
}
|
||||
|
||||
return retErr
|
||||
}
|
||||
|
||||
// Contains returns true if the list contains the given version.
|
||||
func (l List) Contains(version string) bool {
|
||||
for _, v := range l.Versions {
|
||||
if v == version {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// StructuredVersions returns the versions of the list as slice of
|
||||
// Version structs.
|
||||
func (l List) StructuredVersions() []Version {
|
||||
versions := make([]Version, len(l.Versions))
|
||||
for i, v := range l.Versions {
|
||||
versions[i] = Version{
|
||||
Ref: l.Ref,
|
||||
Stream: l.Stream,
|
||||
Version: v,
|
||||
Kind: l.Kind,
|
||||
}
|
||||
}
|
||||
return versions
|
||||
}
|
354
internal/versionsapi/list_test.go
Normal file
354
internal/versionsapi/list_test.go
Normal file
@ -0,0 +1,354 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package versionsapi
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestListJSONPath(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
list List
|
||||
wantPath string
|
||||
}{
|
||||
"major list": {
|
||||
list: List{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Granularity: GranularityMajor,
|
||||
Base: "v1",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/test-ref/stream/nightly/versions/major/v1/image.json",
|
||||
},
|
||||
"minor list": {
|
||||
list: List{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Granularity: GranularityMinor,
|
||||
Base: "v1.1",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/test-ref/stream/nightly/versions/minor/v1.1/image.json",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.wantPath, tc.list.JSONPath())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListURL(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
list List
|
||||
wantURL string
|
||||
wantPath string
|
||||
}{
|
||||
"major list": {
|
||||
list: List{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Granularity: GranularityMajor,
|
||||
Base: "v1",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/test-ref/stream/nightly/versions/major/v1/image.json",
|
||||
},
|
||||
"minor list": {
|
||||
list: List{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Granularity: GranularityMinor,
|
||||
Base: "v1.1",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/test-ref/stream/nightly/versions/minor/v1.1/image.json",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
url, err := tc.list.URL()
|
||||
assert.NoError(err)
|
||||
assert.Equal(tc.wantURL, url)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListValidate(t *testing.T) {
|
||||
majorList := func() *List {
|
||||
return &List{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Granularity: GranularityMajor,
|
||||
Base: "v1",
|
||||
Kind: VersionKindImage,
|
||||
Versions: []string{
|
||||
"v1.0", "v1.1", "v1.2",
|
||||
},
|
||||
}
|
||||
}
|
||||
minorList := func() *List {
|
||||
return &List{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Granularity: GranularityMinor,
|
||||
Base: "v1.1",
|
||||
Kind: VersionKindImage,
|
||||
Versions: []string{
|
||||
"v1.1.0", "v1.1.1", "v1.1.2",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
listFunc func() *List
|
||||
overrideFunc func(list *List)
|
||||
wantErr bool
|
||||
}{
|
||||
"valid major list": {
|
||||
listFunc: majorList,
|
||||
},
|
||||
"valid minor list": {
|
||||
listFunc: minorList,
|
||||
},
|
||||
"invalid ref": {
|
||||
listFunc: majorList,
|
||||
overrideFunc: func(list *List) { list.Ref = "" },
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid stream": {
|
||||
listFunc: majorList,
|
||||
overrideFunc: func(list *List) { list.Stream = "invalid" },
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid granularity": {
|
||||
listFunc: majorList,
|
||||
overrideFunc: func(list *List) { list.Granularity = GranularityUnknown },
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid kind": {
|
||||
listFunc: majorList,
|
||||
overrideFunc: func(list *List) { list.Kind = VersionKindUnknown },
|
||||
wantErr: true,
|
||||
},
|
||||
"base ver is not semantic version": {
|
||||
listFunc: majorList,
|
||||
overrideFunc: func(list *List) { list.Base = "invalid" },
|
||||
wantErr: true,
|
||||
},
|
||||
"base ver does not reflect major granularity": {
|
||||
listFunc: majorList,
|
||||
overrideFunc: func(list *List) { list.Base = "v1.0" },
|
||||
wantErr: true,
|
||||
},
|
||||
"base ver does not reflect minor granularity": {
|
||||
listFunc: minorList,
|
||||
overrideFunc: func(list *List) { list.Base = "v1" },
|
||||
wantErr: true,
|
||||
},
|
||||
"version in list is not semantic version": {
|
||||
listFunc: majorList,
|
||||
overrideFunc: func(list *List) { list.Versions[0] = "invalid" },
|
||||
wantErr: true,
|
||||
},
|
||||
"version in list is not sub version of base": {
|
||||
listFunc: majorList,
|
||||
overrideFunc: func(list *List) { list.Versions[0] = "v2.1" },
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
list := tc.listFunc()
|
||||
if tc.overrideFunc != nil {
|
||||
tc.overrideFunc(list)
|
||||
}
|
||||
|
||||
err := list.Validate()
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListValidateRequest(t *testing.T) {
|
||||
majorListReq := func() *List {
|
||||
return &List{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Granularity: GranularityMajor,
|
||||
Base: "v1",
|
||||
Kind: VersionKindImage,
|
||||
}
|
||||
}
|
||||
minorListReq := func() *List {
|
||||
return &List{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Granularity: GranularityMinor,
|
||||
Base: "v1.1",
|
||||
Kind: VersionKindImage,
|
||||
}
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
listFunc func() *List
|
||||
overrideFunc func(list *List)
|
||||
wantErr bool
|
||||
}{
|
||||
"valid major list": {
|
||||
listFunc: majorListReq,
|
||||
},
|
||||
"valid minor list": {
|
||||
listFunc: minorListReq,
|
||||
},
|
||||
"invalid ref": {
|
||||
listFunc: majorListReq,
|
||||
overrideFunc: func(list *List) { list.Ref = "" },
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid stream": {
|
||||
listFunc: majorListReq,
|
||||
overrideFunc: func(list *List) { list.Stream = "invalid" },
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid granularity": {
|
||||
listFunc: majorListReq,
|
||||
overrideFunc: func(list *List) { list.Granularity = GranularityUnknown },
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid kind": {
|
||||
listFunc: majorListReq,
|
||||
overrideFunc: func(list *List) { list.Kind = VersionKindUnknown },
|
||||
wantErr: true,
|
||||
},
|
||||
"base ver is not semantic version": {
|
||||
listFunc: majorListReq,
|
||||
overrideFunc: func(list *List) { list.Base = "invalid" },
|
||||
wantErr: true,
|
||||
},
|
||||
"base ver does not reflect major granularity": {
|
||||
listFunc: majorListReq,
|
||||
overrideFunc: func(list *List) { list.Base = "v1.0" },
|
||||
wantErr: true,
|
||||
},
|
||||
"base ver does not reflect minor granularity": {
|
||||
listFunc: minorListReq,
|
||||
overrideFunc: func(list *List) { list.Base = "v1" },
|
||||
wantErr: true,
|
||||
},
|
||||
"version in list is not empty": {
|
||||
listFunc: majorListReq,
|
||||
overrideFunc: func(list *List) { list.Versions = []string{"v1.1"} },
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
list := tc.listFunc()
|
||||
if tc.overrideFunc != nil {
|
||||
tc.overrideFunc(list)
|
||||
}
|
||||
|
||||
err := list.ValidateRequest()
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListContainer(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
versions []string
|
||||
version string
|
||||
want bool
|
||||
}{
|
||||
"empty list": {
|
||||
versions: []string{},
|
||||
version: "v1.1.1",
|
||||
want: false,
|
||||
},
|
||||
"version not in list": {
|
||||
versions: []string{"v1.1.1"},
|
||||
version: "v1.1.2",
|
||||
want: false,
|
||||
},
|
||||
"version in list": {
|
||||
versions: []string{"v1.1.1", "v1.1.2"},
|
||||
version: "v1.1.1",
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
list := &List{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Granularity: GranularityMinor,
|
||||
Base: "v1.1",
|
||||
Kind: VersionKindImage,
|
||||
Versions: tc.versions,
|
||||
}
|
||||
|
||||
assert.Equal(tc.want, list.Contains(tc.version))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListStructuredVersions(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
list := List{
|
||||
Ref: "test-ref",
|
||||
Stream: "nightly",
|
||||
Granularity: GranularityMinor,
|
||||
Base: "v1.1",
|
||||
Kind: VersionKindImage,
|
||||
Versions: []string{"v1.1.1", "v1.1.2", "v1.1.3", "v1.1.4", "v1.1.5"},
|
||||
}
|
||||
|
||||
versions := list.StructuredVersions()
|
||||
assert.Len(versions, 5)
|
||||
|
||||
verStrs := make([]string, len(versions))
|
||||
for i, v := range versions {
|
||||
assert.Equal(list.Ref, v.Ref)
|
||||
assert.Equal(list.Stream, v.Stream)
|
||||
assert.Equal(list.Kind, v.Kind)
|
||||
verStrs[i] = v.Version
|
||||
}
|
||||
|
||||
assert.ElementsMatch(list.Versions, verStrs)
|
||||
}
|
370
internal/versionsapi/version.go
Normal file
370
internal/versionsapi/version.go
Normal file
@ -0,0 +1,370 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package versionsapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"go.uber.org/multierr"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// ReleaseRef is the ref used for release versions.
|
||||
const ReleaseRef = "-"
|
||||
|
||||
// Version represents a version. A version has a ref, stream, version string and kind.
|
||||
//
|
||||
// Notice that version is a meta object to the versions API and there isn't an
|
||||
// actual corresponding object in the S3 bucket.
|
||||
// Therefore, the version object doesn't have a URL or JSON path.
|
||||
type Version struct {
|
||||
Ref string
|
||||
Stream string
|
||||
Version string
|
||||
Kind VersionKind
|
||||
}
|
||||
|
||||
// NewVersionFromShortPath creates a new Version from a version short path.
|
||||
func NewVersionFromShortPath(shortPath string, kind VersionKind) (Version, error) {
|
||||
ref, stream, version, err := parseShortPath(shortPath)
|
||||
if err != nil {
|
||||
return Version{}, err
|
||||
}
|
||||
|
||||
ver := Version{
|
||||
Ref: ref,
|
||||
Stream: stream,
|
||||
Version: version,
|
||||
Kind: kind,
|
||||
}
|
||||
|
||||
if err := ver.Validate(); err != nil {
|
||||
return Version{}, err
|
||||
}
|
||||
|
||||
return ver, nil
|
||||
}
|
||||
|
||||
// ShortPath returns the short path of the version.
|
||||
func (v Version) ShortPath() string {
|
||||
return shortPath(v.Ref, v.Stream, v.Version)
|
||||
}
|
||||
|
||||
// Validate validates the version.
|
||||
func (v Version) Validate() error {
|
||||
var retErr error
|
||||
if err := ValidateRef(v.Ref); err != nil {
|
||||
retErr = multierr.Append(retErr, err)
|
||||
}
|
||||
if err := ValidateStream(v.Ref, v.Stream); err != nil {
|
||||
retErr = multierr.Append(retErr, err)
|
||||
}
|
||||
if !semver.IsValid(v.Version) {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("version %q is not a valid semantic version", v.Version))
|
||||
}
|
||||
if semver.Canonical(v.Version) != v.Version {
|
||||
retErr = multierr.Append(retErr, fmt.Errorf("version %q is not a canonical semantic version", v.Version))
|
||||
}
|
||||
if v.Kind == VersionKindUnknown {
|
||||
retErr = multierr.Append(retErr, errors.New("version kind is unknown"))
|
||||
}
|
||||
return retErr
|
||||
}
|
||||
|
||||
// Major returns the major version corresponding to the version.
|
||||
// For example, if the version is "v1.2.3", the major version is "v1".
|
||||
func (v Version) Major() string {
|
||||
return semver.Major(v.Version)
|
||||
}
|
||||
|
||||
// MajorMinor returns the major and minor version corresponding to the version.
|
||||
// For example, if the version is "v1.2.3", the major and minor version is "v1.2".
|
||||
func (v Version) MajorMinor() string {
|
||||
return semver.MajorMinor(v.Version)
|
||||
}
|
||||
|
||||
// WithGranularity returns the version with the given granularity.
|
||||
//
|
||||
// For example, if the version is "v1.2.3" and the granularity is GranularityMajor,
|
||||
// the returned version is "v1".
|
||||
// This is a helper function for Major() and MajorMinor() and v.Version.
|
||||
// In case of an unknown granularity, an empty string is returned.
|
||||
func (v Version) WithGranularity(gran Granularity) string {
|
||||
switch gran {
|
||||
case GranularityMajor:
|
||||
return v.Major()
|
||||
case GranularityMinor:
|
||||
return v.MajorMinor()
|
||||
case GranularityPatch:
|
||||
return v.Version
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ListURL returns the URL of the list with the given granularity,
|
||||
// this version is listed in.
|
||||
// For example, assing GranularityMajor returns the URL of the versions list
|
||||
// that maps the major version of this version to its minor version.
|
||||
// In case of an unknown granularity, an empty string is returned.
|
||||
func (v Version) ListURL(gran Granularity) string {
|
||||
if gran == GranularityUnknown || gran == GranularityPatch {
|
||||
return ""
|
||||
}
|
||||
return constants.CDNRepositoryURL + "/" + v.ListPath(gran)
|
||||
}
|
||||
|
||||
// ListPath returns the path of the list with the given granularity,
|
||||
// this version is listed in.
|
||||
// For example, assing GranularityMajor returns the path of the versions list
|
||||
// that maps the major version of this version to its minor version.
|
||||
// In case of an unknown granularity, an empty string is returned.
|
||||
func (v Version) ListPath(gran Granularity) string {
|
||||
if gran == GranularityUnknown || gran == GranularityPatch {
|
||||
return ""
|
||||
}
|
||||
return path.Join(
|
||||
constants.CDNAPIPrefix,
|
||||
"ref", v.Ref,
|
||||
"stream", v.Stream,
|
||||
"versions",
|
||||
gran.String(), v.WithGranularity(gran),
|
||||
v.Kind.String()+".json",
|
||||
)
|
||||
}
|
||||
|
||||
// ImagePath returns the path to the image specification of this version.
|
||||
// The path points to a directory and is intended to be used for deletion.
|
||||
//
|
||||
// TODO(katexochen): Refactor path and make this a unified path for all version kinds.
|
||||
func (v Version) ImagePath() string {
|
||||
return path.Join(
|
||||
constants.CDNAPIPrefix,
|
||||
"ref", v.Ref,
|
||||
"stream", v.Stream,
|
||||
"image", v.Version,
|
||||
)
|
||||
}
|
||||
|
||||
// VersionKind represents the kind of resource the version versions.
|
||||
type VersionKind int
|
||||
|
||||
const (
|
||||
// VersionKindUnknown is the default value for VersionKind.
|
||||
VersionKindUnknown VersionKind = iota
|
||||
// VersionKindImage is the kind for image versions.
|
||||
VersionKindImage
|
||||
)
|
||||
|
||||
// MarshalJSON marshals the VersionKind to JSON.
|
||||
func (k VersionKind) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(k.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals the VersionKind from JSON.
|
||||
func (k *VersionKind) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
*k = VersionKindFromString(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns the string representation of the VersionKind.
|
||||
func (k VersionKind) String() string {
|
||||
switch k {
|
||||
case VersionKindImage:
|
||||
return "image"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// VersionKindFromString returns the VersionKind for the given string.
|
||||
func VersionKindFromString(s string) VersionKind {
|
||||
switch strings.ToLower(s) {
|
||||
case "image":
|
||||
return VersionKindImage
|
||||
default:
|
||||
return VersionKindUnknown
|
||||
}
|
||||
}
|
||||
|
||||
// Granularity represents the granularity of a semantic version.
|
||||
type Granularity int
|
||||
|
||||
const (
|
||||
// GranularityUnknown is the default granularity.
|
||||
GranularityUnknown Granularity = iota
|
||||
// GranularityMajor is the granularity of a major version, e.g. "v1".
|
||||
// Lists with granularity "major" map from a major version to a list of minor versions.
|
||||
GranularityMajor
|
||||
// GranularityMinor is the granularity of a minor version, e.g. "v1.0".
|
||||
// Lists with granularity "minor" map from a minor version to a list of patch versions.
|
||||
GranularityMinor
|
||||
// GranularityPatch is the granularity of a patch version, e.g. "v1.0.0".
|
||||
// There are no lists with granularity "patch".
|
||||
GranularityPatch
|
||||
)
|
||||
|
||||
// MarshalJSON marshals the granularity to JSON.
|
||||
func (g Granularity) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(g.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals the granularity from JSON.
|
||||
func (g *Granularity) UnmarshalJSON(data []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
*g = GranularityFromString(s)
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns the string representation of the granularity.
|
||||
func (g Granularity) String() string {
|
||||
switch g {
|
||||
case GranularityMajor:
|
||||
return "major"
|
||||
case GranularityMinor:
|
||||
return "minor"
|
||||
case GranularityPatch:
|
||||
return "patch"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// GranularityFromString returns the granularity for the given string.
|
||||
func GranularityFromString(s string) Granularity {
|
||||
switch strings.ToLower(s) {
|
||||
case "major":
|
||||
return GranularityMajor
|
||||
case "minor":
|
||||
return GranularityMinor
|
||||
case "patch":
|
||||
return GranularityPatch
|
||||
default:
|
||||
return GranularityUnknown
|
||||
}
|
||||
}
|
||||
|
||||
var notAZ09Regexp = regexp.MustCompile("[^a-zA-Z0-9-]")
|
||||
|
||||
// CanonicalizeRef returns the canonicalized ref for the given ref.
|
||||
func CanonicalizeRef(ref string) string {
|
||||
if ref == ReleaseRef {
|
||||
return ref
|
||||
}
|
||||
|
||||
canRef := notAZ09Regexp.ReplaceAllString(ref, "-")
|
||||
|
||||
if canRef == ReleaseRef {
|
||||
return "" // No ref should be cannonicalized to the release ref.
|
||||
}
|
||||
|
||||
return canRef
|
||||
}
|
||||
|
||||
// ValidateRef checks if the given ref is a valid ref.
|
||||
// Canonicalize the ref before checking if it is valid.
|
||||
func ValidateRef(ref string) error {
|
||||
if ref == "" {
|
||||
return errors.New("ref must not be empty")
|
||||
}
|
||||
|
||||
if notAZ09Regexp.FindString(ref) != "" {
|
||||
return errors.New("ref must only contain alphanumeric characters and dashes")
|
||||
}
|
||||
|
||||
if strings.HasPrefix(ref, "refs-heads") {
|
||||
return errors.New("ref must not start with 'refs-heads'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateStream checks if the given stream is a valid stream for the given ref.
|
||||
func ValidateStream(ref, stream string) error {
|
||||
validReleaseStreams := []string{"stable", "console", "debug"}
|
||||
validStreams := []string{"nightly", "console", "debug"}
|
||||
|
||||
if ref == ReleaseRef {
|
||||
validStreams = validReleaseStreams
|
||||
}
|
||||
|
||||
for _, validStream := range validStreams {
|
||||
if stream == validStream {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("stream %q is unknown or not supported on ref %q", stream, ref)
|
||||
}
|
||||
|
||||
var (
|
||||
shortPathRegex = regexp.MustCompile(`^ref/([a-zA-Z0-9-]+)/stream/([a-zA-Z0-9-]+)/([a-zA-Z0-9.-]+)$`)
|
||||
shortPathReleaseRegex = regexp.MustCompile(`^stream/([a-zA-Z0-9-]+)/([a-zA-Z0-9.-]+)$`)
|
||||
)
|
||||
|
||||
func shortPath(ref, stream, version string) string {
|
||||
var sp string
|
||||
if ref != ReleaseRef {
|
||||
sp = path.Join("ref", ref)
|
||||
}
|
||||
if stream != "stable" {
|
||||
sp = path.Join(sp, "stream", stream)
|
||||
}
|
||||
return path.Join(sp, version)
|
||||
}
|
||||
|
||||
func parseShortPath(shortPath string) (ref, stream, version string, err error) {
|
||||
if shortPathRegex.MatchString(shortPath) {
|
||||
matches := shortPathRegex.FindStringSubmatch(shortPath)
|
||||
ref := matches[1]
|
||||
if err := ValidateRef(ref); err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
stream := matches[2]
|
||||
if err := ValidateStream(ref, stream); err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
version := matches[3]
|
||||
if !semver.IsValid(version) {
|
||||
return "", "", "", fmt.Errorf("invalid version %q", version)
|
||||
}
|
||||
|
||||
return ref, stream, version, nil
|
||||
}
|
||||
|
||||
if shortPathReleaseRegex.MatchString(shortPath) {
|
||||
matches := shortPathReleaseRegex.FindStringSubmatch(shortPath)
|
||||
stream := matches[1]
|
||||
if err := ValidateStream(ref, stream); err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
version := matches[2]
|
||||
if !semver.IsValid(version) {
|
||||
return "", "", "", fmt.Errorf("invalid version %q", version)
|
||||
}
|
||||
return ReleaseRef, stream, version, nil
|
||||
}
|
||||
|
||||
if semver.IsValid(shortPath) {
|
||||
return ReleaseRef, "stable", shortPath, nil
|
||||
}
|
||||
|
||||
return "", "", "", fmt.Errorf("invalid short path %q", shortPath)
|
||||
}
|
684
internal/versionsapi/version_test.go
Normal file
684
internal/versionsapi/version_test.go
Normal file
@ -0,0 +1,684 @@
|
||||
/*
|
||||
Copyright (c) Edgeless Systems GmbH
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package versionsapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewVersionFromShortPath(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
path string
|
||||
kind VersionKind
|
||||
wantVer Version
|
||||
wantErr bool
|
||||
}{
|
||||
"stable release image": {
|
||||
path: "v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
wantVer: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
},
|
||||
"release debug image": {
|
||||
path: "stream/debug/v9.9.9",
|
||||
kind: VersionKindImage,
|
||||
wantVer: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
},
|
||||
"unknown kind": {
|
||||
path: "v9.9.9",
|
||||
kind: VersionKindUnknown,
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid path": {
|
||||
path: "va.b.c",
|
||||
kind: VersionKindImage,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ver, err := NewVersionFromShortPath(tc.path, tc.kind)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
assert.Equal(tc.wantVer, ver)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionShortPath(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
ver Version
|
||||
want string
|
||||
}{
|
||||
"stable release image": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
want: "v9.9.9",
|
||||
},
|
||||
"release debug image": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
want: "stream/debug/v9.9.9",
|
||||
},
|
||||
"branch image": {
|
||||
ver: Version{
|
||||
Ref: "foo",
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
want: "ref/foo/stream/debug/v9.9.9",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
got := tc.ver.ShortPath()
|
||||
assert.Equal(tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionValidate(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
ver Version
|
||||
wantErr bool
|
||||
}{
|
||||
"valid": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
},
|
||||
"invalid ref": {
|
||||
ver: Version{
|
||||
Ref: "foo/bar",
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid stream": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "foo/bar",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid version": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9/foo",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
"invalid kind": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindUnknown,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
err := tc.ver.Validate()
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
return
|
||||
}
|
||||
assert.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionMajor(t *testing.T) {
|
||||
testCases := map[string]string{
|
||||
"v9.9.9": "v9",
|
||||
"v9.6.9-foo": "v9",
|
||||
"v7.9.9": "v7",
|
||||
}
|
||||
|
||||
for version, major := range testCases {
|
||||
t.Run(version, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ver := Version{Version: version}
|
||||
assert.Equal(major, ver.Major())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionMajorMinor(t *testing.T) {
|
||||
testCases := map[string]string{
|
||||
"v9.9.9": "v9.9",
|
||||
"v9.6.9-foo": "v9.6",
|
||||
"v7.9.9": "v7.9",
|
||||
}
|
||||
|
||||
for version, major := range testCases {
|
||||
t.Run(version, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ver := Version{Version: version}
|
||||
assert.Equal(major, ver.MajorMinor())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionWithGranularity(t *testing.T) {
|
||||
testCases := []struct {
|
||||
ver string
|
||||
gran Granularity
|
||||
want string
|
||||
}{
|
||||
{
|
||||
ver: "v9.9.9",
|
||||
gran: GranularityMajor,
|
||||
want: "v9",
|
||||
},
|
||||
{
|
||||
ver: "v9.9.9",
|
||||
gran: GranularityMinor,
|
||||
want: "v9.9",
|
||||
},
|
||||
{
|
||||
ver: "v9.9.9",
|
||||
gran: GranularityPatch,
|
||||
want: "v9.9.9",
|
||||
},
|
||||
{
|
||||
ver: "v9.9.9-foo",
|
||||
gran: GranularityMajor,
|
||||
want: "v9",
|
||||
},
|
||||
{
|
||||
ver: "v9.9.9-foo",
|
||||
gran: GranularityPatch,
|
||||
want: "v9.9.9-foo",
|
||||
},
|
||||
{
|
||||
ver: "v9.9.9-foo",
|
||||
gran: GranularityUnknown,
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.ver, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ver := Version{Version: tc.ver}
|
||||
assert.Equal(tc.want, ver.WithGranularity(tc.gran))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionListPathURL(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
ver Version
|
||||
gran Granularity
|
||||
wantPath string
|
||||
wantURL string
|
||||
}{
|
||||
"release": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityMajor,
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/stable/versions/major/v9/image.json",
|
||||
wantURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/stable/versions/major/v9/image.json",
|
||||
},
|
||||
"release with minor": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityMinor,
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/stable/versions/minor/v9.9/image.json",
|
||||
wantURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/stable/versions/minor/v9.9/image.json",
|
||||
},
|
||||
"release with patch": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityPatch,
|
||||
wantPath: "",
|
||||
wantURL: "",
|
||||
},
|
||||
"release with unknown": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityUnknown,
|
||||
wantPath: "",
|
||||
wantURL: "",
|
||||
},
|
||||
"release debug stream": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityMajor,
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/debug/versions/major/v9/image.json",
|
||||
wantURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/debug/versions/major/v9/image.json",
|
||||
},
|
||||
"release debug stream with minor": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityMinor,
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/debug/versions/minor/v9.9/image.json",
|
||||
wantURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/debug/versions/minor/v9.9/image.json",
|
||||
},
|
||||
"branch ref": {
|
||||
ver: Version{
|
||||
Ref: "foo",
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityMajor,
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/foo/stream/debug/versions/major/v9/image.json",
|
||||
wantURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/foo/stream/debug/versions/major/v9/image.json",
|
||||
},
|
||||
"branch ref with minor": {
|
||||
ver: Version{
|
||||
Ref: "foo",
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
gran: GranularityMinor,
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/foo/stream/debug/versions/minor/v9.9/image.json",
|
||||
wantURL: constants.CDNRepositoryURL + "/" + constants.CDNAPIPrefix + "/ref/foo/stream/debug/versions/minor/v9.9/image.json",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("%s url", name), func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
url := tc.ver.ListURL(tc.gran)
|
||||
assert.Equal(tc.wantURL, url)
|
||||
})
|
||||
|
||||
t.Run(fmt.Sprintf("%s path", name), func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
path := tc.ver.ListPath(tc.gran)
|
||||
assert.Equal(tc.wantPath, path)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionImagePath(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
ver Version
|
||||
wantPath string
|
||||
}{
|
||||
"release": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "stable",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/stable/image/v9.9.9",
|
||||
},
|
||||
"release debug stream": {
|
||||
ver: Version{
|
||||
Ref: ReleaseRef,
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/" + ReleaseRef + "/stream/debug/image/v9.9.9",
|
||||
},
|
||||
"branch ref": {
|
||||
ver: Version{
|
||||
Ref: "foo",
|
||||
Stream: "debug",
|
||||
Version: "v9.9.9",
|
||||
Kind: VersionKindImage,
|
||||
},
|
||||
wantPath: constants.CDNAPIPrefix + "/ref/foo/stream/debug/image/v9.9.9",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
path := tc.ver.ImagePath()
|
||||
assert.Equal(tc.wantPath, path)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionKindUnMarshalJson(t *testing.T) {
|
||||
testCases := map[string]VersionKind{
|
||||
`"image"`: VersionKindImage,
|
||||
`"unknown"`: VersionKindUnknown,
|
||||
}
|
||||
|
||||
for name, kind := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
data, err := kind.MarshalJSON()
|
||||
assert.NoError(err)
|
||||
assert.Equal(name, string(data))
|
||||
|
||||
var gotKind VersionKind
|
||||
err = gotKind.UnmarshalJSON([]byte(name))
|
||||
assert.NoError(err)
|
||||
assert.Equal(kind, gotKind)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionKindFromString(t *testing.T) {
|
||||
testCases := map[string]VersionKind{
|
||||
"image": VersionKindImage,
|
||||
"unknown": VersionKindUnknown,
|
||||
}
|
||||
|
||||
for name, kind := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
s := kind.String()
|
||||
assert.Equal(name, s)
|
||||
|
||||
k := VersionKindFromString(name)
|
||||
assert.Equal(kind, k)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGranularityUnMarschalJSON(t *testing.T) {
|
||||
testCases := map[string]Granularity{
|
||||
`"major"`: GranularityMajor,
|
||||
`"minor"`: GranularityMinor,
|
||||
`"patch"`: GranularityPatch,
|
||||
`"unknown"`: GranularityUnknown,
|
||||
}
|
||||
|
||||
for name, gran := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
data, err := gran.MarshalJSON()
|
||||
assert.NoError(err)
|
||||
assert.Equal(name, string(data))
|
||||
|
||||
var gotGran Granularity
|
||||
err = gotGran.UnmarshalJSON([]byte(name))
|
||||
assert.NoError(err)
|
||||
assert.Equal(gran, gotGran)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGranularityFromString(t *testing.T) {
|
||||
testCases := map[string]Granularity{
|
||||
"major": GranularityMajor,
|
||||
"minor": GranularityMinor,
|
||||
"patch": GranularityPatch,
|
||||
"unknown": GranularityUnknown,
|
||||
}
|
||||
|
||||
for name, gran := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
s := gran.String()
|
||||
assert.Equal(name, s)
|
||||
|
||||
g := GranularityFromString(name)
|
||||
assert.Equal(gran, g)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalRef(t *testing.T) {
|
||||
testCases := map[string]string{
|
||||
"feat/foo": "feat-foo",
|
||||
"feat-foo": "feat-foo",
|
||||
"feat$foo": "feat-foo",
|
||||
"3234": "3234",
|
||||
"feat foo": "feat-foo",
|
||||
"/../": "----",
|
||||
ReleaseRef: ReleaseRef,
|
||||
".": "",
|
||||
}
|
||||
|
||||
for ref, want := range testCases {
|
||||
t.Run(ref, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
assert.Equal(want, CanonicalizeRef(ref))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRef(t *testing.T) {
|
||||
testCases := map[string]bool{
|
||||
"feat/foo": true,
|
||||
"feat-foo": false,
|
||||
"feat$foo": true,
|
||||
"3234": false,
|
||||
"feat foo": true,
|
||||
"refs-heads-feat-foo": true,
|
||||
"": true,
|
||||
}
|
||||
|
||||
for ref, wantErr := range testCases {
|
||||
t.Run(ref, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
err := ValidateRef(ref)
|
||||
if !wantErr {
|
||||
assert.NoError(err)
|
||||
} else {
|
||||
assert.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateStream(t *testing.T) {
|
||||
testCases := []struct {
|
||||
branch string
|
||||
stream string
|
||||
wantErr bool
|
||||
}{
|
||||
{branch: "-", stream: "stable", wantErr: false},
|
||||
{branch: "-", stream: "debug", wantErr: false},
|
||||
{branch: "-", stream: "nightly", wantErr: true},
|
||||
{branch: "-", stream: "console", wantErr: false},
|
||||
{branch: "main", stream: "stable", wantErr: true},
|
||||
{branch: "main", stream: "debug", wantErr: false},
|
||||
{branch: "main", stream: "nightly", wantErr: false},
|
||||
{branch: "main", stream: "console", wantErr: false},
|
||||
{branch: "foo-branch", stream: "nightly", wantErr: false},
|
||||
{branch: "foo-branch", stream: "console", wantErr: false},
|
||||
{branch: "foo-branch", stream: "debug", wantErr: false},
|
||||
{branch: "foo-branch", stream: "stable", wantErr: true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.branch+"+"+tc.stream, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
err := ValidateStream(tc.branch, tc.stream)
|
||||
if !tc.wantErr {
|
||||
assert.NoError(err)
|
||||
} else {
|
||||
assert.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortPath(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
ref string
|
||||
stream string
|
||||
version string
|
||||
}{
|
||||
"v9.9.9": {
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9",
|
||||
},
|
||||
"v9.9.9-foo": {
|
||||
ref: ReleaseRef,
|
||||
stream: "stable",
|
||||
version: "v9.9.9-foo",
|
||||
},
|
||||
"stream/debug/v9.9.9": {
|
||||
ref: ReleaseRef,
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
},
|
||||
"ref/foo/stream/debug/v9.9.9": {
|
||||
ref: "foo",
|
||||
stream: "debug",
|
||||
version: "v9.9.9",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
path := shortPath(tc.ref, tc.stream, tc.version)
|
||||
assert.Equal(name, path)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseShortPath(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
wantRef string
|
||||
wantStream string
|
||||
wantVersion string
|
||||
wantErr bool
|
||||
}{
|
||||
"v9.9.9": {
|
||||
wantRef: ReleaseRef,
|
||||
wantStream: "stable",
|
||||
wantVersion: "v9.9.9",
|
||||
},
|
||||
"v9.9.9-foo": {
|
||||
wantRef: ReleaseRef,
|
||||
wantStream: "stable",
|
||||
wantVersion: "v9.9.9-foo",
|
||||
},
|
||||
"stream/debug/v9.9.9": {
|
||||
wantRef: ReleaseRef,
|
||||
wantStream: "debug",
|
||||
wantVersion: "v9.9.9",
|
||||
},
|
||||
"ref/foo/stream/debug/v9.9.9": {
|
||||
wantRef: "foo",
|
||||
wantStream: "debug",
|
||||
wantVersion: "v9.9.9",
|
||||
},
|
||||
"v9.9.9-foo/bar": {
|
||||
wantErr: true,
|
||||
},
|
||||
"ref/foo/stream/debug/va.b.9": {
|
||||
wantErr: true,
|
||||
},
|
||||
"stream/debug/va.b.9": {
|
||||
wantErr: true,
|
||||
},
|
||||
"ref/foo/stream/bar/v9.9.9": {
|
||||
wantErr: true,
|
||||
},
|
||||
"stream/bar/v9.9.9": {
|
||||
wantErr: true,
|
||||
},
|
||||
"ref/refs-heads-bar/stream/debug/v9.9.9": {
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
ref, stream, version, err := parseShortPath(name)
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
assert.Equal(tc.wantRef, ref)
|
||||
assert.Equal(tc.wantStream, stream)
|
||||
assert.Equal(tc.wantVersion, version)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user