mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-16 19:34:37 -05:00
459 lines
12 KiB
Go
459 lines
12 KiB
Go
/*
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
package versionsapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"path"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"golang.org/x/mod/semver"
|
|
|
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
|
)
|
|
|
|
// 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.
|
|
//
|
|
// Versions fields are private so the type can be used in other packages by
|
|
// defining private interfaces.
|
|
type Version struct {
|
|
ref string
|
|
stream string
|
|
version string
|
|
kind VersionKind
|
|
}
|
|
|
|
// NewVersion creates a new Version object and validates it.
|
|
func NewVersion(ref, stream, version string, kind VersionKind) (Version, error) {
|
|
ver := Version{
|
|
ref: ref,
|
|
stream: stream,
|
|
version: version,
|
|
kind: kind,
|
|
}
|
|
|
|
if err := ver.Validate(); err != nil {
|
|
return Version{}, err
|
|
}
|
|
|
|
return ver, nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Ref returns the ref of the version.
|
|
func (v Version) Ref() string {
|
|
return v.ref
|
|
}
|
|
|
|
// Stream returns the stream of the version.
|
|
func (v Version) Stream() string {
|
|
return v.stream
|
|
}
|
|
|
|
// Version returns the version string of the version.
|
|
func (v Version) Version() string {
|
|
return v.version
|
|
}
|
|
|
|
// Kind returns the kind of the version.
|
|
func (v Version) Kind() VersionKind {
|
|
return v.kind
|
|
}
|
|
|
|
// 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 = errors.Join(retErr, err)
|
|
}
|
|
if err := ValidateStream(v.ref, v.stream); err != nil {
|
|
retErr = errors.Join(retErr, err)
|
|
}
|
|
if !semver.IsValid(v.version) {
|
|
retErr = errors.Join(retErr, fmt.Errorf("version %q is not a valid semantic version", v.version))
|
|
}
|
|
if semver.Canonical(v.version) != v.version {
|
|
retErr = errors.Join(retErr, fmt.Errorf("version %q is not a canonical semantic version", v.version))
|
|
}
|
|
if v.kind == VersionKindUnknown {
|
|
retErr = errors.Join(retErr, errors.New("version kind is unknown"))
|
|
}
|
|
return retErr
|
|
}
|
|
|
|
// Equal returns true if the versions are equal.
|
|
func (v Version) Equal(other Version) bool {
|
|
return v.ref == other.ref &&
|
|
v.stream == other.stream &&
|
|
v.version == other.version &&
|
|
v.kind == other.kind
|
|
}
|
|
|
|
// 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",
|
|
)
|
|
}
|
|
|
|
// ArtifactsURL returns the URL to the artifacts stored for this version.
|
|
// The URL points to a directory.
|
|
func (v Version) ArtifactsURL(apiVer apiVersion) string {
|
|
return constants.CDNRepositoryURL + "/" + v.ArtifactPath(apiVer)
|
|
}
|
|
|
|
// ArtifactPath returns the path to the artifacts stored for this version.
|
|
// The path points to a directory.
|
|
func (v Version) ArtifactPath(apiVer apiVersion) string {
|
|
return path.Join(
|
|
constants.CDNAPIBase,
|
|
apiVer.String(),
|
|
"ref", v.ref,
|
|
"stream", v.stream,
|
|
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
|
|
// VersionKindCLI is the kind for CLI versions.
|
|
VersionKindCLI
|
|
)
|
|
|
|
// 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"
|
|
case VersionKindCLI:
|
|
return "cli"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// VersionKindFromString returns the VersionKind for the given string.
|
|
func VersionKindFromString(s string) VersionKind {
|
|
switch strings.ToLower(s) {
|
|
case "image":
|
|
return VersionKindImage
|
|
case "cli":
|
|
return VersionKindCLI
|
|
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)
|
|
}
|
|
|
|
// MeasurementURL builds the measurement and signature URLs for the given version.
|
|
func MeasurementURL(version Version) (measurementURL, signatureURL *url.URL, err error) {
|
|
if version.kind != VersionKindImage {
|
|
return &url.URL{}, &url.URL{}, fmt.Errorf("kind %q is not supported", version.kind)
|
|
}
|
|
|
|
measurementPath, err := url.JoinPath(version.ArtifactsURL(APIV2), "image", constants.CDNMeasurementsFile)
|
|
if err != nil {
|
|
return &url.URL{}, &url.URL{}, fmt.Errorf("joining path for measurement: %w", err)
|
|
}
|
|
signaturePath, err := url.JoinPath(version.ArtifactsURL(APIV2), "image", constants.CDNMeasurementsSignature)
|
|
if err != nil {
|
|
return &url.URL{}, &url.URL{}, fmt.Errorf("joining path for signature: %w", err)
|
|
}
|
|
|
|
measurementURL, err = url.Parse(measurementPath)
|
|
if err != nil {
|
|
return &url.URL{}, &url.URL{}, fmt.Errorf("parsing path for measurement: %w", err)
|
|
}
|
|
|
|
signatureURL, err = url.Parse(signaturePath)
|
|
if err != nil {
|
|
return &url.URL{}, &url.URL{}, fmt.Errorf("parsing path for signature: %w", err)
|
|
}
|
|
return measurementURL, signatureURL, nil
|
|
}
|
|
|
|
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 {
|
|
return path.Join("ref", ref, "stream", stream, version)
|
|
}
|
|
|
|
if stream != "stable" {
|
|
return path.Join(sp, "stream", stream, version)
|
|
}
|
|
|
|
return 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)
|
|
}
|