/* 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" "log/slog" "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/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 *slog.Logger } // New creates a new Uploader. func New(uplosiPath string, log *slog.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) openstackConfig := baseConfig["openstack"].(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) extendOpenStackConfig(openstackConfig, 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") } // ..