diff --git a/cli/internal/cmd/apply.go b/cli/internal/cmd/apply.go
index ea7679a50..b15ae249d 100644
--- a/cli/internal/cmd/apply.go
+++ b/cli/internal/cmd/apply.go
@@ -40,7 +40,7 @@ import (
"github.com/edgelesssys/constellation/v2/internal/kms/uri"
"github.com/edgelesssys/constellation/v2/internal/semver"
"github.com/edgelesssys/constellation/v2/internal/versions"
- "github.com/samber/slog-multi"
+ slogmulti "github.com/samber/slog-multi"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@@ -365,7 +365,7 @@ func (a *applyCmd) apply(
}
// Check license
- a.checkLicenseFile(cmd, conf.GetProvider())
+ a.checkLicenseFile(cmd, conf.GetProvider(), conf.UseMarketplaceImage())
// Now start actually running the apply command
diff --git a/cli/internal/cmd/license_enterprise.go b/cli/internal/cmd/license_enterprise.go
index 79ae2bf7c..d4afe973e 100644
--- a/cli/internal/cmd/license_enterprise.go
+++ b/cli/internal/cmd/license_enterprise.go
@@ -22,18 +22,22 @@ import (
// with the license server. If no license file is present or if errors
// occur during the check, the user is informed and the community license
// is used. It is a no-op in the open source version of Constellation.
-func (a *applyCmd) checkLicenseFile(cmd *cobra.Command, csp cloudprovider.Provider) {
+func (a *applyCmd) checkLicenseFile(cmd *cobra.Command, csp cloudprovider.Provider, useMarketplaceImage bool) {
var licenseID string
a.log.Debug("Running license check")
readBytes, err := a.fileHandler.Read(constants.LicenseFilename)
- if errors.Is(err, fs.ErrNotExist) {
- cmd.Printf("Using community license.\n")
+ switch {
+ case useMarketplaceImage:
+ cmd.Println("Using marketplace image billing.")
+ licenseID = license.MarketplaceLicense
+ case errors.Is(err, fs.ErrNotExist):
+ cmd.Println("Using community license.")
licenseID = license.CommunityLicense
- } else if err != nil {
+ case err != nil:
cmd.Printf("Error: %v\nContinuing with community license.\n", err)
licenseID = license.CommunityLicense
- } else {
+ default:
cmd.Printf("Constellation license found!\n")
licenseID, err = license.FromBytes(readBytes)
if err != nil {
@@ -43,9 +47,11 @@ func (a *applyCmd) checkLicenseFile(cmd *cobra.Command, csp cloudprovider.Provid
}
quota, err := a.applier.CheckLicense(cmd.Context(), csp, !a.flags.skipPhases.contains(skipInitPhase), licenseID)
- if err != nil {
+ if err != nil && !useMarketplaceImage {
cmd.Printf("Unable to contact license server.\n")
cmd.Printf("Please keep your vCPU quota in mind.\n")
+ } else if licenseID == license.MarketplaceLicense {
+ // Do nothing. Billing is handled by the marketplace.
} else if licenseID == license.CommunityLicense {
cmd.Printf("For details, see https://docs.edgeless.systems/constellation/overview/license\n")
} else {
diff --git a/cli/internal/cmd/license_oss.go b/cli/internal/cmd/license_oss.go
index 8fba56114..fd14d35bc 100644
--- a/cli/internal/cmd/license_oss.go
+++ b/cli/internal/cmd/license_oss.go
@@ -17,4 +17,4 @@ import (
// with the license server. If no license file is present or if errors
// occur during the check, the user is informed and the community license
// is used. It is a no-op in the open source version of Constellation.
-func (a *applyCmd) checkLicenseFile(*cobra.Command, cloudprovider.Provider) {}
+func (a *applyCmd) checkLicenseFile(*cobra.Command, cloudprovider.Provider, bool) {}
diff --git a/internal/config/config.go b/internal/config/config.go
index 611ccc39f..d4a8cab40 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -720,7 +720,8 @@ func (c *Config) DeployYawolLoadBalancer() bool {
func (c *Config) UseMarketplaceImage() bool {
return (c.Provider.Azure != nil && c.Provider.Azure.UseMarketplaceImage != nil && *c.Provider.Azure.UseMarketplaceImage) ||
(c.Provider.GCP != nil && c.Provider.GCP.UseMarketplaceImage != nil && *c.Provider.GCP.UseMarketplaceImage) ||
- (c.Provider.AWS != nil && c.Provider.AWS.UseMarketplaceImage != nil && *c.Provider.AWS.UseMarketplaceImage)
+ (c.Provider.AWS != nil && c.Provider.AWS.UseMarketplaceImage != nil && *c.Provider.AWS.UseMarketplaceImage) ||
+ (c.Provider.OpenStack != nil && c.Provider.OpenStack.Cloud == "stackit")
}
// Validate checks the config values and returns validation errors.
diff --git a/internal/imagefetcher/imagefetcher.go b/internal/imagefetcher/imagefetcher.go
index 643e7c1b4..2d1364ca5 100644
--- a/internal/imagefetcher/imagefetcher.go
+++ b/internal/imagefetcher/imagefetcher.go
@@ -131,6 +131,9 @@ func buildMarketplaceImage(payload marketplaceImagePayload) (string, error) {
case cloudprovider.AWS:
// For AWS, we use the AMI alias, which just needs the version and infers the rest transparently.
return fmt.Sprintf("resolve:ssm:/aws/service/marketplace/prod-77ylkenlkgufs/%s", payload.imgInfo.Version), nil
+ case cloudprovider.OpenStack:
+ // For OpenStack / STACKIT, we use the image reference directly.
+ return getReferenceFromImageInfo(payload.provider, payload.attestationVariant.String(), payload.imgInfo, payload.filters...)
default:
return "", fmt.Errorf("marketplace images are not supported for csp %s", payload.provider.String())
}
diff --git a/internal/license/license.go b/internal/license/license.go
index 0bf1cb3fe..0010bd2d0 100644
--- a/internal/license/license.go
+++ b/internal/license/license.go
@@ -13,6 +13,8 @@ type Action string
const (
// CommunityLicense is used by everyone who has not bought an enterprise license.
CommunityLicense = "00000000-0000-0000-0000-000000000000"
+ // MarketplaceLicense is used by everyone who uses a marketplace image.
+ MarketplaceLicense = "11111111-1111-1111-1111-111111111111"
// Init action denotes the initialization of a Constellation cluster.
Init Action = "init"
diff --git a/terraform-provider-constellation/docs/data-sources/attestation.md b/terraform-provider-constellation/docs/data-sources/attestation.md
index bd578314c..7ad4d491e 100644
--- a/terraform-provider-constellation/docs/data-sources/attestation.md
+++ b/terraform-provider-constellation/docs/data-sources/attestation.md
@@ -58,6 +58,10 @@ Required:
- `$SEMANTIC_VERSION` is the semantic version of the image, e.g. `vX.Y.Z` or `vX.Y.Z-pre...`.
- `version` (String) Semantic version of the image.
+Optional:
+
+- `marketplace_image` (Boolean) Whether a marketplace image should be used.
+
### Nested Schema for `attestation`
diff --git a/terraform-provider-constellation/docs/data-sources/image.md b/terraform-provider-constellation/docs/data-sources/image.md
index 8eb48929e..d72b9ca91 100644
--- a/terraform-provider-constellation/docs/data-sources/image.md
+++ b/terraform-provider-constellation/docs/data-sources/image.md
@@ -49,6 +49,10 @@ The Constellation OS image must be [replicated to the region](https://docs.edgel
### Nested Schema for `image`
+Optional:
+
+- `marketplace_image` (Boolean) Whether a marketplace image should be used.
+
Read-Only:
- `reference` (String) CSP-specific unique reference to the image. The format differs per CSP.
diff --git a/terraform-provider-constellation/docs/resources/cluster.md b/terraform-provider-constellation/docs/resources/cluster.md
index d5deed553..282493ce8 100644
--- a/terraform-provider-constellation/docs/resources/cluster.md
+++ b/terraform-provider-constellation/docs/resources/cluster.md
@@ -162,6 +162,10 @@ Required:
- `$SEMANTIC_VERSION` is the semantic version of the image, e.g. `vX.Y.Z` or `vX.Y.Z-pre...`.
- `version` (String) Semantic version of the image.
+Optional:
+
+- `marketplace_image` (Boolean) Whether a marketplace image should be used.
+
### Nested Schema for `network_config`
diff --git a/terraform-provider-constellation/internal/provider/cluster_resource.go b/terraform-provider-constellation/internal/provider/cluster_resource.go
index 9f51aa848..f2dfb91c8 100644
--- a/terraform-provider-constellation/internal/provider/cluster_resource.go
+++ b/terraform-provider-constellation/internal/provider/cluster_resource.go
@@ -447,28 +447,31 @@ func (r *ClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPla
return
}
- licenseID := plannedState.LicenseID.ValueString()
- if licenseID == "" {
- resp.Diagnostics.AddWarning("Constellation license ID not set.",
- "Continuing with community license.")
- }
- if licenseID == license.CommunityLicense {
- resp.Diagnostics.AddWarning("Using community license.",
- "For details, see https://docs.edgeless.systems/constellation/overview/license")
- }
-
// Validate during plan. Must be done in ModifyPlan to read provider data.
// See https://developer.hashicorp.com/terraform/plugin/framework/resources/configure#define-resource-configure-method.
_, diags := r.getMicroserviceVersion(&plannedState)
resp.Diagnostics.Append(diags...)
- _, _, diags = r.getImageVersion(ctx, &plannedState)
+ var image imageAttribute
+ image, _, diags = r.getImageVersion(ctx, &plannedState)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
+ licenseID := plannedState.LicenseID.ValueString()
+ switch {
+ case image.MarketplaceImage != nil && *image.MarketplaceImage:
+ // Marketplace images do not require a license.
+ case licenseID == "":
+ resp.Diagnostics.AddWarning("Constellation license ID not set.",
+ "Continuing with community license.")
+ case licenseID == license.CommunityLicense:
+ resp.Diagnostics.AddWarning("Using community license.",
+ "For details, see https://docs.edgeless.systems/constellation/overview/license")
+ }
+
// Checks running on updates to the resource. (i.e. state and plan != nil)
if !req.State.Raw.IsNull() {
// Read currentState supplied by Terraform runtime into the model
@@ -759,9 +762,13 @@ func (r *ClusterResource) apply(ctx context.Context, data *ClusterResourceModel,
// parse license ID
licenseID := data.LicenseID.ValueString()
- if licenseID == "" {
+ switch {
+ case image.MarketplaceImage != nil && *image.MarketplaceImage:
+ licenseID = license.MarketplaceLicense
+ case licenseID == "":
licenseID = license.CommunityLicense
}
+
// license ID can be base64-encoded
licenseIDFromB64, err := base64.StdEncoding.DecodeString(licenseID)
if err == nil {
diff --git a/terraform-provider-constellation/internal/provider/cluster_resource_test.go b/terraform-provider-constellation/internal/provider/cluster_resource_test.go
index 9cc197bb5..d9df71713 100644
--- a/terraform-provider-constellation/internal/provider/cluster_resource_test.go
+++ b/terraform-provider-constellation/internal/provider/cluster_resource_test.go
@@ -97,9 +97,10 @@ func TestViolatedImageConstraint(t *testing.T) {
}
input, diags := basetypes.NewObjectValueFrom(context.Background(), map[string]attr.Type{
- "version": basetypes.StringType{},
- "reference": basetypes.StringType{},
- "short_path": basetypes.StringType{},
+ "version": basetypes.StringType{},
+ "reference": basetypes.StringType{},
+ "short_path": basetypes.StringType{},
+ "marketplace_image": basetypes.BoolType{},
}, img)
require.Equal(t, 0, diags.ErrorsCount())
_, _, diags2 := sut.getImageVersion(context.Background(), &ClusterResourceModel{
diff --git a/terraform-provider-constellation/internal/provider/shared_attributes.go b/terraform-provider-constellation/internal/provider/shared_attributes.go
index 79535a53c..163794e9b 100644
--- a/terraform-provider-constellation/internal/provider/shared_attributes.go
+++ b/terraform-provider-constellation/internal/provider/shared_attributes.go
@@ -229,13 +229,19 @@ func newImageAttributeSchema(t attributeType) schema.Attribute {
Computed: !isInput,
Required: isInput,
},
+ "marketplace_image": schema.BoolAttribute{
+ Description: "Whether a marketplace image should be used.",
+ MarkdownDescription: "Whether a marketplace image should be used.",
+ Optional: true,
+ },
},
}
}
// imageAttribute is the image attribute's data model.
type imageAttribute struct {
- Reference string `tfsdk:"reference"`
- Version string `tfsdk:"version"`
- ShortPath string `tfsdk:"short_path"`
+ Reference string `tfsdk:"reference"`
+ Version string `tfsdk:"version"`
+ ShortPath string `tfsdk:"short_path"`
+ MarketplaceImage *bool `tfsdk:"marketplace_image"`
}