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"` }