mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
image: add image uploader that uses uplosi in the background
This implementation will replace the custom Go code in internal/osimage/{aws|azure|gcp} and still conforms to the same interface.
This commit is contained in:
parent
181b8f64d2
commit
fb392c2d50
2
go.mod
2
go.mod
@ -176,7 +176,7 @@ require (
|
|||||||
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
github.com/Azure/go-autorest/logger v0.2.1 // indirect
|
||||||
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
|
||||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect
|
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect
|
||||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
github.com/BurntSushi/toml v1.3.2
|
||||||
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
github.com/MakeNowJust/heredoc v1.0.0 // indirect
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
github.com/Masterminds/semver/v3 v3.2.1 // indirect
|
||||||
|
16
internal/osimage/uplosi/BUILD.bazel
Normal file
16
internal/osimage/uplosi/BUILD.bazel
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||||
|
|
||||||
|
go_library(
|
||||||
|
name = "uplosi",
|
||||||
|
srcs = ["uplosiupload.go"],
|
||||||
|
embedsrcs = ["uplosi.conf.in"],
|
||||||
|
importpath = "github.com/edgelesssys/constellation/v2/internal/osimage/uplosi",
|
||||||
|
visibility = ["//:__subpackages__"],
|
||||||
|
deps = [
|
||||||
|
"//internal/api/versionsapi",
|
||||||
|
"//internal/cloud/cloudprovider",
|
||||||
|
"//internal/logger",
|
||||||
|
"//internal/osimage",
|
||||||
|
"@com_github_burntsushi_toml//:toml",
|
||||||
|
],
|
||||||
|
)
|
21
internal/osimage/uplosi/uplosi.conf.in
Normal file
21
internal/osimage/uplosi/uplosi.conf.in
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[base]
|
||||||
|
name = "constellation"
|
||||||
|
|
||||||
|
[base.aws]
|
||||||
|
region = "eu-central-1"
|
||||||
|
replicationRegions = ["eu-west-1", "eu-west-3", "us-east-2", "ap-south-1"]
|
||||||
|
bucket = "constellation-images"
|
||||||
|
publish = true
|
||||||
|
|
||||||
|
[base.azure]
|
||||||
|
subscriptionID = "0d202bbb-4fa7-4af8-8125-58c269a05435"
|
||||||
|
location = "northeurope"
|
||||||
|
resourceGroup = "constellation-images"
|
||||||
|
sharingNamePrefix = "constellation"
|
||||||
|
sku = "constellation"
|
||||||
|
publisher = "edgelesssys"
|
||||||
|
|
||||||
|
[base.gcp]
|
||||||
|
project = "constellation-images"
|
||||||
|
location = "europe-west3"
|
||||||
|
bucket = "constellation-os-images"
|
258
internal/osimage/uplosi/uplosiupload.go
Normal file
258
internal/osimage/uplosi/uplosiupload.go
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
// package uplosi implements uploading os images using uplosi.
|
||||||
|
package uplosi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/osimage"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed uplosi.conf.in
|
||||||
|
var uplosiConfigTemplate string
|
||||||
|
|
||||||
|
const timestampFormat = "20060102150405"
|
||||||
|
|
||||||
|
// Uploader can upload os images using uplosi.
|
||||||
|
type Uploader struct {
|
||||||
|
uplosiPath string
|
||||||
|
|
||||||
|
log *logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Uploader.
|
||||||
|
func New(uplosiPath string, log *logger.Logger) *Uploader {
|
||||||
|
return &Uploader{
|
||||||
|
uplosiPath: uplosiPath,
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload uploads the given os image using uplosi.
|
||||||
|
func (u *Uploader) Upload(ctx context.Context, req *osimage.UploadRequest) ([]versionsapi.ImageInfoEntry, error) {
|
||||||
|
config, err := prepareUplosiConfig(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace, err := prepareWorkspace(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(workspace)
|
||||||
|
|
||||||
|
uplosiOutput, err := runUplosi(ctx, u.uplosiPath, workspace, req.ImagePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseUplosiOutput(uplosiOutput, req.Provider, req.AttestationVariant)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareUplosiConfig(req *osimage.UploadRequest) ([]byte, error) {
|
||||||
|
var config map[string]any
|
||||||
|
if _, err := toml.Decode(uplosiConfigTemplate, &config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
imageVersionStr, err := imageVersion(req.Provider, req.Version, req.Timestamp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
baseConfig := config["base"].(map[string]any)
|
||||||
|
awsConfig := baseConfig["aws"].(map[string]any)
|
||||||
|
azureConfig := baseConfig["azure"].(map[string]any)
|
||||||
|
gcpConfig := baseConfig["gcp"].(map[string]any)
|
||||||
|
|
||||||
|
baseConfig["imageVersion"] = imageVersionStr
|
||||||
|
baseConfig["provider"] = strings.ToLower(req.Provider.String())
|
||||||
|
extendAWSConfig(awsConfig, req.Version, req.AttestationVariant, req.Timestamp)
|
||||||
|
extendAzureConfig(azureConfig, req.Version, req.AttestationVariant, req.Timestamp)
|
||||||
|
extendGCPConfig(gcpConfig, req.Version, req.AttestationVariant)
|
||||||
|
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
if err := toml.NewEncoder(buf).Encode(config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareWorkspace(config []byte) (string, error) {
|
||||||
|
workspace, err := os.MkdirTemp("", "uplosi-")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// write config to workspace
|
||||||
|
configPath := filepath.Join(workspace, "uplosi.conf")
|
||||||
|
if err := os.WriteFile(configPath, config, 0o644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return workspace, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runUplosi(ctx context.Context, uplosiPath string, workspace string, rawImage string) ([]byte, error) {
|
||||||
|
imagePath, err := filepath.Abs(rawImage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
uplosiCmd := exec.CommandContext(ctx, uplosiPath, "upload", imagePath)
|
||||||
|
uplosiCmd.Dir = workspace
|
||||||
|
uplosiCmd.Stderr = os.Stderr
|
||||||
|
return uplosiCmd.Output()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUplosiOutput(output []byte, csp cloudprovider.Provider, attestationVariant string) ([]versionsapi.ImageInfoEntry, error) {
|
||||||
|
lines := strings.Split(string(output), "\n")
|
||||||
|
var imageReferences []versionsapi.ImageInfoEntry
|
||||||
|
for _, line := range lines {
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var region, reference string
|
||||||
|
if csp == cloudprovider.AWS {
|
||||||
|
var err error
|
||||||
|
region, reference, err = awsParseAMIARN(line)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
reference = line
|
||||||
|
}
|
||||||
|
imageReferences = append(imageReferences, versionsapi.ImageInfoEntry{
|
||||||
|
CSP: strings.ToLower(csp.String()),
|
||||||
|
AttestationVariant: attestationVariant,
|
||||||
|
Reference: reference,
|
||||||
|
Region: region,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return imageReferences, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageVersion(csp cloudprovider.Provider, version versionsapi.Version, timestamp time.Time) (string, error) {
|
||||||
|
cleanSemver := strings.TrimPrefix(regexp.MustCompile(`^v\d+\.\d+\.\d+`).FindString(version.Version()), "v")
|
||||||
|
if csp != cloudprovider.Azure {
|
||||||
|
return cleanSemver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case version.Stream() == "stable":
|
||||||
|
fallthrough
|
||||||
|
case version.Stream() == "debug" && version.Ref() == "-":
|
||||||
|
return cleanSemver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedTime := timestamp.Format(timestampFormat)
|
||||||
|
if len(formattedTime) != len(timestampFormat) {
|
||||||
|
return "", errors.New("invalid timestamp")
|
||||||
|
}
|
||||||
|
// <year>.<month><day>.<time>
|
||||||
|
return formattedTime[:4] + "." + formattedTime[4:8] + "." + formattedTime[8:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extendAWSConfig(awsConfig map[string]any, version versionsapi.Version, attestationVariant string, timestamp time.Time) {
|
||||||
|
awsConfig["amiName"] = awsAMIName(version, attestationVariant, timestamp)
|
||||||
|
awsConfig["snapshotName"] = awsAMIName(version, attestationVariant, timestamp)
|
||||||
|
awsConfig["blobName"] = fmt.Sprintf("image-%s-%s-%d.raw", version.Stream(), version.Version(), timestamp.Unix())
|
||||||
|
}
|
||||||
|
|
||||||
|
func awsAMIName(version versionsapi.Version, attestationVariant string, timestamp time.Time) string {
|
||||||
|
if version.Stream() == "stable" {
|
||||||
|
return fmt.Sprintf("constellation-%s-%s", version.Version(), attestationVariant)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("constellation-%s-%s-%s-%s", version.Stream(), version.Version(), attestationVariant, timestamp.Format(timestampFormat))
|
||||||
|
}
|
||||||
|
|
||||||
|
func awsParseAMIARN(arn string) (region string, amiID string, retErr error) {
|
||||||
|
parts := strings.Split(arn, ":")
|
||||||
|
if len(parts) != 6 {
|
||||||
|
return "", "", fmt.Errorf("invalid ARN (expected 5 path components) %q", arn)
|
||||||
|
}
|
||||||
|
if parts[0] != "arn" {
|
||||||
|
return "", "", fmt.Errorf("invalid ARN (prefix mismatch) %q", arn)
|
||||||
|
}
|
||||||
|
if parts[1] != "aws" {
|
||||||
|
return "", "", fmt.Errorf("invalid ARN (provider mismatch) %q", arn)
|
||||||
|
}
|
||||||
|
if parts[2] != "ec2" {
|
||||||
|
return "", "", fmt.Errorf("invalid ARN (service mismatch) %q", arn)
|
||||||
|
}
|
||||||
|
resourceParts := strings.Split(parts[5], "/")
|
||||||
|
if len(resourceParts) != 2 {
|
||||||
|
return "", "", fmt.Errorf("invalid ARN (expected resource type/id) %q", arn)
|
||||||
|
}
|
||||||
|
if resourceParts[0] != "image" {
|
||||||
|
return "", "", fmt.Errorf("invalid ARN (resource type mismatch) %q", arn)
|
||||||
|
}
|
||||||
|
return parts[3], resourceParts[1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extendAzureConfig(azureConfig map[string]any, version versionsapi.Version, attestationVariant string, timestamp time.Time) {
|
||||||
|
azureConfig["attestationVariant"] = attestationVariant
|
||||||
|
azureConfig["sharedImageGallery"] = azureGalleryName(version)
|
||||||
|
azureConfig["imageDefinitionName"] = azureImageOffer(version)
|
||||||
|
azureConfig["offer"] = azureImageOffer(version)
|
||||||
|
formattedTime := timestamp.Format(timestampFormat)
|
||||||
|
azureConfig["diskName"] = fmt.Sprintf("constellation-%s-%s-%s", version.Stream(), formattedTime, attestationVariant)
|
||||||
|
}
|
||||||
|
|
||||||
|
func azureGalleryName(version versionsapi.Version) string {
|
||||||
|
switch version.Stream() {
|
||||||
|
case "stable":
|
||||||
|
return "Constellation_CVM"
|
||||||
|
case "debug":
|
||||||
|
return "Constellation_Debug_CVM"
|
||||||
|
}
|
||||||
|
return "Constellation_Testing_CVM"
|
||||||
|
}
|
||||||
|
|
||||||
|
func azureImageOffer(version versionsapi.Version) string {
|
||||||
|
switch {
|
||||||
|
case version.Stream() == "stable":
|
||||||
|
return "constellation"
|
||||||
|
case version.Stream() == "debug" && version.Ref() == "-":
|
||||||
|
return version.Version()
|
||||||
|
}
|
||||||
|
return version.Ref() + "-" + version.Stream()
|
||||||
|
}
|
||||||
|
|
||||||
|
func extendGCPConfig(gcpConfig map[string]any, version versionsapi.Version, attestationVariant string) {
|
||||||
|
gcpConfig["imageFamily"] = gcpImageFamily(version)
|
||||||
|
gcpConfig["imageName"] = gcpImageName(version, attestationVariant)
|
||||||
|
gcpConfig["blobName"] = gcpImageName(version, attestationVariant) + ".tar.gz"
|
||||||
|
}
|
||||||
|
|
||||||
|
func gcpImageFamily(version versionsapi.Version) string {
|
||||||
|
if version.Stream() == "stable" {
|
||||||
|
return "constellation"
|
||||||
|
}
|
||||||
|
truncatedRef := version.Ref()
|
||||||
|
if len(version.Ref()) > 45 {
|
||||||
|
truncatedRef = version.Ref()[:45]
|
||||||
|
}
|
||||||
|
return "constellation-" + truncatedRef
|
||||||
|
}
|
||||||
|
|
||||||
|
func gcpImageName(version versionsapi.Version, attestationVariant string) string {
|
||||||
|
return strings.ReplaceAll(version.Version(), ".", "-") + "-" + attestationVariant + "-" + version.Stream()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user