Feat/version manifests (#387)

Signed-off-by: Fabian Kammel <fk@edgeless.systems>
Co-authored-by: Otto Bittner <cobittner@posteo.net>
This commit is contained in:
Fabian Kammel 2022-08-23 13:19:37 +02:00 committed by GitHub
parent cdcbed6ff9
commit ec79484948
8 changed files with 377 additions and 0 deletions

View File

@ -0,0 +1,9 @@
# Build Manifests
This tool will fetch all supported versions for all released Constellation versions.
## Usage
```sh
AZURE_SUBSCRIPTION_ID=<AZURE_SUBSCRIPTION_ID> go run . | jq
```

View File

@ -0,0 +1,65 @@
package azure
import (
"context"
"fmt"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v2"
"github.com/edgelesssys/constellation/internal/logger"
)
type Client struct {
log *logger.Logger
opts Options
versionClient *armcompute.GalleryImageVersionsClient
}
func NewClient(log *logger.Logger, opts Options) *Client {
log = log.Named("azure-client")
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
log.Fatalf("unable to create default credentials: %v", err)
}
versionClient, err := armcompute.NewGalleryImageVersionsClient(opts.SubscriptionID, cred, &arm.ClientOptions{})
if err != nil {
log.Fatalf("unable to create version client: %v", err)
}
return &Client{
log: log,
opts: opts,
versionClient: versionClient,
}
}
func (c *Client) FetchImages(ctx context.Context) (map[string]string, error) {
ctx, cancel := context.WithTimeout(ctx, time.Second*5)
defer cancel()
imageVersionPager := c.versionClient.NewListByGalleryImagePager(
c.opts.ResourceGroupName,
c.opts.GalleryName,
c.opts.ImageDefinition,
&armcompute.GalleryImageVersionsClientListByGalleryImageOptions{},
)
images := map[string]string{}
for imageVersionPager.More() {
imageVersionPage, err := imageVersionPager.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("failed to advance page: %v", err)
}
for _, imageVersion := range imageVersionPage.Value {
imageName := "v" + *imageVersion.Name
images[imageName] = *imageVersion.ID
}
}
return images, nil
}

View File

@ -0,0 +1,37 @@
package azure
import (
"fmt"
"github.com/google/uuid"
)
const (
DefaultResourceGroupName = "CONSTELLATION-IMAGES"
DefaultGalleryName = "Constellation"
DefaultImageDefinition = "constellation"
)
type Options struct {
SubscriptionID string
ResourceGroupName string
GalleryName string
ImageDefinition string
}
func DefaultOptions() Options {
return Options{
SubscriptionID: "",
ResourceGroupName: DefaultResourceGroupName,
GalleryName: DefaultGalleryName,
ImageDefinition: DefaultImageDefinition,
}
}
func (o *Options) SetSubscription(sub string) error {
if _, err := uuid.Parse(sub); err != nil {
return fmt.Errorf("unable to set subscription: %w", err)
}
o.SubscriptionID = sub
return nil
}

View File

@ -0,0 +1,63 @@
package gcp
import (
"context"
"strings"
compute "cloud.google.com/go/compute/apiv1"
"github.com/edgelesssys/constellation/internal/logger"
"google.golang.org/api/iterator"
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
)
type Client struct {
client *compute.ImagesClient
log *logger.Logger
opts Options
}
func NewClient(ctx context.Context, log *logger.Logger, opts Options) *Client {
client, err := compute.NewImagesRESTClient(ctx)
if err != nil {
log.Fatalf("Unable to create GCP client: %v", err)
}
return &Client{
client: client,
log: log,
opts: opts,
}
}
func (c *Client) Close() error {
return c.client.Close()
}
func (c *Client) FetchImages(ctx context.Context) (map[string]string, error) {
imgIterator := c.client.List(ctx, &computepb.ListImagesRequest{
Project: c.opts.ProjectID,
})
images := map[string]string{}
for {
img, err := imgIterator.Next()
if err == iterator.Done {
break
}
if err != nil {
c.log.Fatalf("unable to request image: %v", err)
}
if img == nil || *img.Family != c.opts.ImageFamily {
continue
}
imgReference := strings.TrimPrefix(*img.SelfLink, "https://www.googleapis.com/compute/v1/")
imgVersion, err := c.opts.Filter(imgReference)
if err != nil {
continue
}
images[imgVersion] = imgReference
}
return images, nil
}

View File

@ -0,0 +1,37 @@
package gcp
import (
"fmt"
"regexp"
"strings"
)
const (
DefaultProjectID = "constellation-images"
DefaultImageFamily = "constellation"
)
type Options struct {
ProjectID string
ImageFamily string
Filter func(image string) (version string, err error)
}
func DefaultOptions() Options {
return Options{
ProjectID: DefaultProjectID,
ImageFamily: DefaultImageFamily,
Filter: isGcpReleaseImage,
}
}
func isGcpReleaseImage(image string) (imageVersion string, err error) {
isReleaseRegEx := regexp.MustCompile(`^projects\/constellation-images\/global\/images\/constellation-v[\d]+-[\d]+-[\d]+$`)
if !isReleaseRegEx.MatchString(image) {
return "", fmt.Errorf("image does not look like release image")
}
findVersionRegEx := regexp.MustCompile(`v[\d]+-[\d]+-[\d]+$`)
version := findVersionRegEx.FindString(image)
semVer := strings.ReplaceAll(version, "-", ".")
return semVer, nil
}

View File

@ -0,0 +1,38 @@
package gcp
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsGcpReleaseImage(t *testing.T) {
testCases := map[string]struct {
image string
wantVersion string
wantError bool
}{
"works for release image": {
image: "projects/constellation-images/global/images/constellation-v1-3-0",
wantVersion: "v1.3.0",
},
"breaks for debug image": {
image: "projects/constellation-images/global/images/constellation-20220805151600",
wantError: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
version, err := isGcpReleaseImage(tc.image)
if tc.wantError {
assert.Error(err)
return
}
assert.NoError(err)
assert.Equal(tc.wantVersion, version)
})
}
}

View File

@ -0,0 +1,59 @@
package main
import (
"context"
"encoding/json"
"os"
"github.com/edgelesssys/constellation/hack/build-manifest/azure"
"github.com/edgelesssys/constellation/hack/build-manifest/gcp"
"github.com/edgelesssys/constellation/internal/logger"
"go.uber.org/zap/zapcore"
)
const (
AzureSubscriptionIDEnv = "AZURE_SUBSCRIPTION_ID"
)
func main() {
ctx := context.Background()
log := logger.New(logger.PlainLog, zapcore.InfoLevel)
manifests := OldManifests()
fetchAzureImages(ctx, manifests, log)
fetchGCPImages(ctx, manifests, log)
if err := json.NewEncoder(os.Stdout).Encode(&manifests); err != nil {
log.Fatalf("%v", err)
}
}
func fetchAzureImages(ctx context.Context, manifests Manifest, log *logger.Logger) {
options := azure.DefaultOptions()
if err := options.SetSubscription(os.Getenv(AzureSubscriptionIDEnv)); err != nil {
log.Fatalf("please provide a valid subscription UUID via '%s' envar", AzureSubscriptionIDEnv)
}
client := azure.NewClient(log, options)
images, err := client.FetchImages(ctx)
if err != nil {
log.Fatalf("unable to fetch Azure image: %v", err)
}
for version, image := range images {
manifests.SetAzureImage(version, image)
}
}
func fetchGCPImages(ctx context.Context, manifests Manifest, log *logger.Logger) {
options := gcp.DefaultOptions()
client := gcp.NewClient(ctx, log, options)
images, err := client.FetchImages(ctx)
if err != nil {
log.Fatalf("unable to fetch GCP images: %v", err)
}
for version, image := range images {
manifests.SetGCPImage(version, image)
}
}

View File

@ -0,0 +1,69 @@
package main
import "encoding/json"
type Manifest struct {
releases map[string]Images
}
type Images struct {
AzureCoreosImage string `json:"AzureCoreOSImage"`
GCPCoreOSImage string `json:"GCPCoreOSImage"`
}
// OldManifests provides Constellation releases to image mapping. These are the
// default images configured for each release.
func OldManifests() Manifest {
return Manifest{
releases: map[string]Images{
"v1.0.0": {
AzureCoreosImage: "/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/CONSTELLATION-IMAGES/providers/Microsoft.Compute/galleries/Constellation/images/constellation-coreos/versions/0.0.1651150807",
GCPCoreOSImage: "constellation-coreos-1651150807",
},
"v1.1.0": {
AzureCoreosImage: "/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/CONSTELLATION-IMAGES/providers/Microsoft.Compute/galleries/Constellation/images/constellation-coreos/versions/0.0.1654096948",
GCPCoreOSImage: "projects/constellation-images/global/images/constellation-coreos-1654096948",
},
"v1.2.0": {
AzureCoreosImage: "/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/CONSTELLATION-IMAGES/providers/Microsoft.Compute/galleries/Constellation/images/constellation-coreos/versions/0.0.1654162332",
GCPCoreOSImage: "projects/constellation-images/global/images/constellation-coreos-1654162332",
},
"v1.3.0": {
AzureCoreosImage: "/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/CONSTELLATION-IMAGES/providers/Microsoft.Compute/galleries/Constellation/images/constellation-coreos/versions/0.0.1654162332",
GCPCoreOSImage: "projects/constellation-images/global/images/constellation-coreos-1654162332",
},
"v1.3.1": {
AzureCoreosImage: "/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/CONSTELLATION-IMAGES/providers/Microsoft.Compute/galleries/Constellation/images/constellation-coreos/versions/0.0.1657199013",
GCPCoreOSImage: "projects/constellation-images/global/images/constellation-coreos-1657199013",
},
"v1.4.0": {
AzureCoreosImage: "/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/CONSTELLATION-IMAGES/providers/Microsoft.Compute/galleries/Constellation/images/constellation-coreos/versions/0.0.1659453699",
GCPCoreOSImage: "projects/constellation-images/global/images/constellation-coreos-1659453699",
},
},
}
}
func (m *Manifest) MarshalJSON() ([]byte, error) {
return json.Marshal(m.releases)
}
func (m *Manifest) SetAzureImage(version string, image string) {
if release, ok := m.releases[version]; !ok {
images := Images{AzureCoreosImage: image}
m.releases[version] = images
} else {
release.AzureCoreosImage = image
m.releases[version] = release
}
}
func (m *Manifest) SetGCPImage(version string, image string) {
if release, ok := m.releases[version]; !ok {
images := Images{GCPCoreOSImage: image}
m.releases[version] = images
} else {
release.GCPCoreOSImage = image
m.releases[version] = release
}
}