/* Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ package versionsapi import ( "errors" "fmt" "net/url" "path" "sort" "github.com/edgelesssys/constellation/v2/internal/constants" "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"` // List contains the image variants for this version. List []ImageInfoEntry `json:"list,omitempty"` } // ImageInfoEntry contains information about a single image variant. type ImageInfoEntry struct { CSP string `json:"csp"` AttestationVariant string `json:"attestationVariant"` Reference string `json:"reference"` Region string `json:"region,omitempty"` } // JSONPath returns the S3 JSON path for this object. func (i ImageInfo) JSONPath() string { return path.Join( constants.CDNAPIPrefixV2, "ref", i.Ref, "stream", i.Stream, i.Version, "image", "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 = errors.Join(retErr, err) } if err := ValidateStream(i.Ref, i.Stream); err != nil { retErr = errors.Join(retErr, err) } if !semver.IsValid(i.Version) { retErr = errors.Join(retErr, fmt.Errorf("version %q is not a valid semver", i.Version)) } if len(i.List) != 0 { retErr = errors.Join(retErr, errors.New("list 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 = errors.Join(retErr, err) } if err := ValidateStream(i.Ref, i.Stream); err != nil { retErr = errors.Join(retErr, err) } if !semver.IsValid(i.Version) { retErr = errors.Join(retErr, fmt.Errorf("version %q is not a valid semver", i.Version)) } if len(i.List) == 0 { retErr = errors.Join(retErr, errors.New("one or more image variants must be specified in the list")) } return retErr } // MergeImageInfos combines the image info entries from multiple sources into a single // image info object. func MergeImageInfos(infos ...ImageInfo) (ImageInfo, error) { if len(infos) == 0 { return ImageInfo{}, errors.New("no image info objects specified") } if len(infos) == 1 { return infos[0], nil } out := ImageInfo{ Ref: infos[0].Ref, Stream: infos[0].Stream, Version: infos[0].Version, List: []ImageInfoEntry{}, } for _, info := range infos { if info.Ref != out.Ref { return ImageInfo{}, errors.New("image info objects have different refs") } if info.Stream != out.Stream { return ImageInfo{}, errors.New("image info objects have different streams") } if info.Version != out.Version { return ImageInfo{}, errors.New("image info objects have different versions") } out.List = append(out.List, info.List...) } sort.SliceStable(out.List, func(i, j int) bool { if out.List[i].CSP != out.List[j].CSP { return out.List[i].CSP < out.List[j].CSP } if out.List[i].AttestationVariant != out.List[j].AttestationVariant { return out.List[i].AttestationVariant < out.List[j].AttestationVariant } if out.List[i].Region != out.List[j].Region { return out.List[i].Region < out.List[j].Region } return out.List[i].Reference < out.List[j].Reference }) return out, nil }