diff --git a/.bazelrc b/.bazelrc index de31dcc06..3c642d5f1 100644 --- a/.bazelrc +++ b/.bazelrc @@ -32,7 +32,7 @@ test --test_tag_filters=-integration # enable all tests (including integration) test:integration --test_tag_filters= --@io_bazel_rules_go//go/config:tags=integration # enable only integration tests -test:integration-only --test_tag_filters=+integration --@io_bazel_rules_go//go/config:tags=integration +test:integration-only --test_tag_filters=+integration --@io_bazel_rules_go//go/config:tags=integration,enterprise # bazel configs to explicitly target a platform common:host --platforms @local_config_platform//:host diff --git a/cli/internal/cmd/apply.go b/cli/internal/cmd/apply.go index e5c8caeac..5b466d2b8 100644 --- a/cli/internal/cmd/apply.go +++ b/cli/internal/cmd/apply.go @@ -244,7 +244,7 @@ func runApply(cmd *cobra.Command, _ []string) error { ) } - applier := constellation.NewApplier(log, spinner, newDialer) + applier := constellation.NewApplier(log, spinner, constellation.ApplyContextCLI, newDialer) apply := &applyCmd{ fileHandler: fileHandler, @@ -824,7 +824,7 @@ type warnLog interface { // applier is used to run the different phases of the apply command. type applier interface { SetKubeConfig(kubeConfig []byte) error - CheckLicense(ctx context.Context, csp cloudprovider.Provider, licenseID string) (int, error) + CheckLicense(ctx context.Context, csp cloudprovider.Provider, initRequest bool, licenseID string) (int, error) // methods required by "init" diff --git a/cli/internal/cmd/apply_test.go b/cli/internal/cmd/apply_test.go index a8e010a03..55f055669 100644 --- a/cli/internal/cmd/apply_test.go +++ b/cli/internal/cmd/apply_test.go @@ -536,7 +536,7 @@ type stubConstellApplier struct { func (s *stubConstellApplier) SetKubeConfig([]byte) error { return nil } -func (s *stubConstellApplier) CheckLicense(context.Context, cloudprovider.Provider, string) (int, error) { +func (s *stubConstellApplier) CheckLicense(context.Context, cloudprovider.Provider, bool, string) (int, error) { return 0, s.checkLicenseErr } diff --git a/cli/internal/cmd/init_test.go b/cli/internal/cmd/init_test.go index 169949d1c..de6278d66 100644 --- a/cli/internal/cmd/init_test.go +++ b/cli/internal/cmd/init_test.go @@ -369,7 +369,7 @@ func TestWriteOutput(t *testing.T) { spinner: &nopSpinner{}, merger: &stubMerger{}, log: logger.NewTest(t), - applier: constellation.NewApplier(logger.NewTest(t), &nopSpinner{}, nil), + applier: constellation.NewApplier(logger.NewTest(t), &nopSpinner{}, constellation.ApplyContextCLI, nil), } err = i.writeInitOutput(stateFile, initOutput, false, &out, measurementSalt) require.NoError(err) @@ -461,7 +461,7 @@ func TestGenerateMasterSecret(t *testing.T) { i := &applyCmd{ fileHandler: fileHandler, log: logger.NewTest(t), - applier: constellation.NewApplier(logger.NewTest(t), &nopSpinner{}, nil), + applier: constellation.NewApplier(logger.NewTest(t), &nopSpinner{}, constellation.ApplyContextCLI, nil), } secret, err := i.generateAndPersistMasterSecret(&out) diff --git a/cli/internal/cmd/license_enterprise.go b/cli/internal/cmd/license_enterprise.go index 2bc1d8797..6ceed9dde 100644 --- a/cli/internal/cmd/license_enterprise.go +++ b/cli/internal/cmd/license_enterprise.go @@ -42,7 +42,7 @@ func (a *applyCmd) checkLicenseFile(cmd *cobra.Command, csp cloudprovider.Provid } } - quota, err := a.applier.CheckLicense(cmd.Context(), csp, licenseID) + quota, err := a.applier.CheckLicense(cmd.Context(), csp, !a.flags.skipPhases.contains(skipInitPhase), licenseID) if err != nil { cmd.Printf("Unable to contact license server.\n") cmd.Printf("Please keep your vCPU quota in mind.\n") diff --git a/internal/constellation/apply.go b/internal/constellation/apply.go index 63751379e..611b1557f 100644 --- a/internal/constellation/apply.go +++ b/internal/constellation/apply.go @@ -20,6 +20,16 @@ import ( "github.com/edgelesssys/constellation/v2/internal/license" ) +// ApplyContext denotes the context in which the apply command is run. +type ApplyContext string + +const ( + // ApplyContextCLI is used when the Applier is used by the CLI. + ApplyContextCLI ApplyContext = "cli" + // ApplyContextTerraform is used when the Applier is used by Terraform. + ApplyContextTerraform ApplyContext = "terraform" +) + // An Applier handles applying a specific configuration to a Constellation cluster // with existing Infrastructure. // In Particular, this involves Initialization and Upgrading of the cluster. @@ -28,6 +38,8 @@ type Applier struct { licenseChecker licenseChecker spinner spinnerInterf + applyContext ApplyContext + // newDialer creates a new aTLS gRPC dialer. newDialer func(validator atls.Validator) *dialer.Dialer kubecmdClient kubecmdClient @@ -35,7 +47,7 @@ type Applier struct { } type licenseChecker interface { - CheckLicense(context.Context, cloudprovider.Provider, string) (license.QuotaCheckResponse, error) + CheckLicense(ctx context.Context, csp cloudprovider.Provider, action license.Action, licenseID string) (int, error) } type debugLog interface { @@ -44,14 +56,15 @@ type debugLog interface { // NewApplier creates a new Applier. func NewApplier( - log debugLog, - spinner spinnerInterf, + log debugLog, spinner spinnerInterf, + applyContext ApplyContext, newDialer func(validator atls.Validator) *dialer.Dialer, ) *Applier { return &Applier{ log: log, spinner: spinner, - licenseChecker: license.NewChecker(license.NewClient()), + licenseChecker: license.NewChecker(), + applyContext: applyContext, newDialer: newDialer, } } @@ -73,15 +86,26 @@ func (a *Applier) SetKubeConfig(kubeConfig []byte) error { // CheckLicense checks the given Constellation license with the license server // and returns the allowed quota for the license. -func (a *Applier) CheckLicense(ctx context.Context, csp cloudprovider.Provider, licenseID string) (int, error) { +func (a *Applier) CheckLicense(ctx context.Context, csp cloudprovider.Provider, initRequest bool, licenseID string) (int, error) { a.log.Debugf("Contacting license server for license '%s'", licenseID) - quotaResp, err := a.licenseChecker.CheckLicense(ctx, csp, licenseID) + + var action license.Action + if initRequest { + action = license.Init + } else { + action = license.Apply + } + if a.applyContext == ApplyContextTerraform { + action += "-terraform" + } + + quota, err := a.licenseChecker.CheckLicense(ctx, csp, action, licenseID) if err != nil { return 0, fmt.Errorf("checking license: %w", err) } a.log.Debugf("Got response from license server for license '%s'", licenseID) - return quotaResp.Quota, nil + return quota, nil } // GenerateMasterSecret generates a new master secret. diff --git a/internal/constellation/apply_test.go b/internal/constellation/apply_test.go index ce2466fdf..54e845033 100644 --- a/internal/constellation/apply_test.go +++ b/internal/constellation/apply_test.go @@ -38,7 +38,7 @@ func TestCheckLicense(t *testing.T) { require := require.New(t) a := &Applier{licenseChecker: tc.licenseChecker, log: logger.NewTest(t)} - _, err := a.CheckLicense(context.Background(), cloudprovider.Unknown, license.CommunityLicense) + _, err := a.CheckLicense(context.Background(), cloudprovider.Unknown, true, license.CommunityLicense) if tc.wantErr { require.Error(err) } else { @@ -52,8 +52,8 @@ type stubLicenseChecker struct { checkLicenseErr error } -func (c *stubLicenseChecker) CheckLicense(context.Context, cloudprovider.Provider, string) (license.QuotaCheckResponse, error) { - return license.QuotaCheckResponse{}, c.checkLicenseErr +func (c *stubLicenseChecker) CheckLicense(context.Context, cloudprovider.Provider, license.Action, string) (int, error) { + return 0, c.checkLicenseErr } func TestGenerateMasterSecret(t *testing.T) { diff --git a/internal/license/BUILD.bazel b/internal/license/BUILD.bazel index 4cd4e56a8..58764259a 100644 --- a/internal/license/BUILD.bazel +++ b/internal/license/BUILD.bazel @@ -22,11 +22,11 @@ go_library( go_test( name = "license_test", - srcs = [ - "file_test.go", - "license_test.go", - ], + srcs = ["file_test.go"], embed = [":license"], + tags = [ + "enterprise", + ], deps = [ "@com_github_stretchr_testify//assert", "@com_github_stretchr_testify//require", diff --git a/internal/license/checker_enterprise.go b/internal/license/checker_enterprise.go index 91b6fd753..f98fe7e98 100644 --- a/internal/license/checker_enterprise.go +++ b/internal/license/checker_enterprise.go @@ -9,26 +9,90 @@ SPDX-License-Identifier: AGPL-3.0-only package license import ( + "bytes" "context" + "encoding/json" + "fmt" + "net/http" + "net/url" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" ) +const ( + apiHost = "license.confidential.cloud" + licensePath = "api/v1/license" +) + +// Checker checks the Constellation license. type Checker struct { - quotaChecker QuotaChecker + httpClient *http.Client } -func NewChecker(quotaChecker QuotaChecker) *Checker { +// NewChecker creates a new Checker. +func NewChecker() *Checker { return &Checker{ - quotaChecker: quotaChecker, + httpClient: http.DefaultClient, } } -// CheckLicense contacts the license server to fetch quota information for the given license. -func (c *Checker) CheckLicense(ctx context.Context, provider cloudprovider.Provider, licenseID string) (QuotaCheckResponse, error) { - return c.quotaChecker.QuotaCheck(ctx, QuotaCheckRequest{ +// CheckLicense checks the Constellation license. If the license is valid, it returns the vCPU quota. +func (c *Checker) CheckLicense(ctx context.Context, csp cloudprovider.Provider, action Action, licenseID string) (int, error) { + checkRequest := quotaCheckRequest{ + Provider: csp.String(), License: licenseID, - Action: Init, - Provider: provider.String(), - }) + Action: action, + } + + reqBody, err := json.Marshal(checkRequest) + if err != nil { + return 0, fmt.Errorf("unable to marshal input: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, licenseURL().String(), bytes.NewBuffer(reqBody)) + if err != nil { + return 0, fmt.Errorf("unable to create request: %w", err) + } + resp, err := c.httpClient.Do(req) + if err != nil { + return 0, fmt.Errorf("unable to do request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return 0, fmt.Errorf("http error %d", resp.StatusCode) + } + + responseContentType := resp.Header.Get("Content-Type") + if responseContentType != "application/json" { + return 0, fmt.Errorf("expected server JSON response but got '%s'", responseContentType) + } + + var parsedResponse quotaCheckResponse + err = json.NewDecoder(resp.Body).Decode(&parsedResponse) + if err != nil { + return 0, fmt.Errorf("unable to parse response: %w", err) + } + + return parsedResponse.Quota, nil +} + +// quotaCheckRequest is JSON request to license server to check quota for a given license and action. +type quotaCheckRequest struct { + Action Action `json:"action"` + Provider string `json:"provider"` + License string `json:"license"` +} + +// quotaCheckResponse is JSON response by license server. +type quotaCheckResponse struct { + Quota int `json:"quota"` +} + +func licenseURL() *url.URL { + return &url.URL{ + Scheme: "https", + Host: apiHost, + Path: licensePath, + } } diff --git a/internal/license/license_test.go b/internal/license/checker_enterprise_test.go similarity index 68% rename from internal/license/license_test.go rename to internal/license/checker_enterprise_test.go index 61667581a..1443ef2f1 100644 --- a/internal/license/license_test.go +++ b/internal/license/checker_enterprise_test.go @@ -1,3 +1,5 @@ +//go:build enterprise + /* Copyright (c) Edgeless Systems GmbH @@ -13,6 +15,7 @@ import ( "net/http" "testing" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/stretchr/testify/assert" ) @@ -25,11 +28,9 @@ func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { } // newTestClient returns *http.Client with Transport replaced to avoid making real calls. -func newTestClient(fn roundTripFunc) *Client { - return &Client{ - httpClient: &http.Client{ - Transport: fn, - }, +func newTestClient(fn roundTripFunc) *http.Client { + return &http.Client{ + Transport: fn, } } @@ -70,31 +71,26 @@ func TestQuotaCheck(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - client := newTestClient(func(req *http.Request) *http.Response { - r := &http.Response{ - StatusCode: tc.serverResponseCode, - Body: io.NopCloser(bytes.NewBufferString(tc.serverResponse)), - Header: make(http.Header), - } - r.Header.Set("Content-Type", tc.serverResponseContent) - return r - }) + client := &Checker{ + httpClient: newTestClient(func(req *http.Request) *http.Response { + r := &http.Response{ + StatusCode: tc.serverResponseCode, + Body: io.NopCloser(bytes.NewBufferString(tc.serverResponse)), + Header: make(http.Header), + } + r.Header.Set("Content-Type", tc.serverResponseContent) + return r + }), + } - resp, err := client.QuotaCheck(context.Background(), QuotaCheckRequest{ - Action: test, - License: tc.license, - }) + quota, err := client.CheckLicense(context.Background(), cloudprovider.Unknown, Init, tc.license) if tc.wantError { assert.Error(err) return } assert.NoError(err) - assert.Equal(tc.wantQuota, resp.Quota) + assert.Equal(tc.wantQuota, quota) }) } } - -func Test_licenseURL(t *testing.T) { - assert.Equal(t, "https://license.confidential.cloud/api/v1/license", licenseURL().String()) -} diff --git a/internal/license/checker_oss.go b/internal/license/checker_oss.go index a7b18327c..58253817e 100644 --- a/internal/license/checker_oss.go +++ b/internal/license/checker_oss.go @@ -18,11 +18,11 @@ import ( type Checker struct{} // NewChecker creates a new Checker. -func NewChecker(QuotaChecker) *Checker { +func NewChecker() *Checker { return &Checker{} } // CheckLicense is a no-op for open source version of Constellation. -func (c *Checker) CheckLicense(context.Context, cloudprovider.Provider, string) (QuotaCheckResponse, error) { - return QuotaCheckResponse{}, nil +func (c *Checker) CheckLicense(context.Context, cloudprovider.Provider, Action, string) (int, error) { + return 0, nil } diff --git a/internal/license/integration/BUILD.bazel b/internal/license/integration/BUILD.bazel index fe71e702a..0a84478da 100644 --- a/internal/license/integration/BUILD.bazel +++ b/internal/license/integration/BUILD.bazel @@ -4,10 +4,12 @@ go_test( name = "integration_test", srcs = ["license_integration_test.go"], tags = [ + "enterprise", "integration", "requires-network", ], deps = [ + "//internal/cloud/cloudprovider", "//internal/license", "@com_github_stretchr_testify//assert", ], diff --git a/internal/license/integration/license_integration_test.go b/internal/license/integration/license_integration_test.go index 7a6158262..f4b67f00d 100644 --- a/internal/license/integration/license_integration_test.go +++ b/internal/license/integration/license_integration_test.go @@ -12,6 +12,7 @@ import ( "context" "testing" + "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/license" "github.com/stretchr/testify/assert" ) @@ -36,20 +37,16 @@ func TestQuotaCheckIntegration(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - client := license.NewClient() + client := license.NewChecker() - req := license.QuotaCheckRequest{ - Action: license.Action("test"), - License: tc.license, - } - resp, err := client.QuotaCheck(context.Background(), req) + quota, err := client.CheckLicense(context.Background(), cloudprovider.Unknown, "test", tc.license) if tc.wantError { assert.Error(err) return } assert.NoError(err) - assert.Equal(tc.wantQuota, resp.Quota) + assert.Equal(tc.wantQuota, quota) }) } } diff --git a/internal/license/license.go b/internal/license/license.go index 1e9525361..0bf1cb3fe 100644 --- a/internal/license/license.go +++ b/internal/license/license.go @@ -7,101 +7,16 @@ SPDX-License-Identifier: AGPL-3.0-only // Package license provides functions to check a user's Constellation license. package license -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "net/http" - "net/url" -) +// Action performed by Constellation. +type Action string const ( // CommunityLicense is used by everyone who has not bought an enterprise license. CommunityLicense = "00000000-0000-0000-0000-000000000000" - apiHost = "license.confidential.cloud" - licensePath = "api/v1/license" -) -type ( - // Action performed by Constellation. - Action string -) - -const ( // Init action denotes the initialization of a Constellation cluster. Init Action = "init" - // test action is only to be used in testing. - test Action = "test" + // Apply action denotes an update of a Constellation cluster. + // It is used after a cluster has already been initialized once. + Apply Action = "apply" ) - -// Client interacts with the ES license server. -type Client struct { - httpClient *http.Client -} - -// NewClient creates a new client to interact with ES license server. -func NewClient() *Client { - return &Client{ - httpClient: http.DefaultClient, - } -} - -// QuotaCheckRequest is JSON request to license server to check quota for a given license and action. -type QuotaCheckRequest struct { - Action Action `json:"action"` - Provider string `json:"provider"` - License string `json:"license"` -} - -// QuotaCheckResponse is JSON response by license server. -type QuotaCheckResponse struct { - Quota int `json:"quota"` -} - -// QuotaCheck for a given license and action, passed via CheckQuotaRequest. -func (c *Client) QuotaCheck(ctx context.Context, checkRequest QuotaCheckRequest) (QuotaCheckResponse, error) { - reqBody, err := json.Marshal(checkRequest) - if err != nil { - return QuotaCheckResponse{}, fmt.Errorf("unable to marshal input: %w", err) - } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, licenseURL().String(), bytes.NewBuffer(reqBody)) - if err != nil { - return QuotaCheckResponse{}, fmt.Errorf("unable to create request: %w", err) - } - resp, err := c.httpClient.Do(req) - if err != nil { - return QuotaCheckResponse{}, fmt.Errorf("unable to do request: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return QuotaCheckResponse{}, fmt.Errorf("http error %d", resp.StatusCode) - } - - responseContentType := resp.Header.Get("Content-Type") - if responseContentType != "application/json" { - return QuotaCheckResponse{}, fmt.Errorf("expected server JSON response but got '%s'", responseContentType) - } - - var parsedResponse QuotaCheckResponse - err = json.NewDecoder(resp.Body).Decode(&parsedResponse) - if err != nil { - return QuotaCheckResponse{}, fmt.Errorf("unable to parse response: %w", err) - } - - return parsedResponse, nil -} - -func licenseURL() *url.URL { - return &url.URL{ - Scheme: "https", - Host: apiHost, - Path: licensePath, - } -} - -// QuotaChecker checks the vCPU quota for a given license. -type QuotaChecker interface { - QuotaCheck(ctx context.Context, checkRequest QuotaCheckRequest) (QuotaCheckResponse, error) -} diff --git a/terraform-provider-constellation/docs/resources/cluster.md b/terraform-provider-constellation/docs/resources/cluster.md index faa4caded..0c17acbc6 100644 --- a/terraform-provider-constellation/docs/resources/cluster.md +++ b/terraform-provider-constellation/docs/resources/cluster.md @@ -83,6 +83,7 @@ resource "constellation_cluster" "azure_example" { - `gcp` (Attributes) GCP-specific configuration. (see [below for nested schema](#nestedatt--gcp)) - `in_cluster_endpoint` (String) The endpoint of the cluster. When not set, the out-of-cluster endpoint is used. - `kubernetes_version` (String) The Kubernetes version to use for the cluster. When not set, version v1.27.8 is used. The supported versions are [v1.26.11 v1.27.8 v1.28.4]. +- `license_id` (String) Constellation license ID. When not set, the community license is used. ### Read-Only diff --git a/terraform-provider-constellation/internal/provider/BUILD.bazel b/terraform-provider-constellation/internal/provider/BUILD.bazel index adeb18985..54063d3f5 100644 --- a/terraform-provider-constellation/internal/provider/BUILD.bazel +++ b/terraform-provider-constellation/internal/provider/BUILD.bazel @@ -33,6 +33,7 @@ go_library( "//internal/grpc/dialer", "//internal/imagefetcher", "//internal/kms/uri", + "//internal/license", "//internal/semver", "//internal/sigstore", "//internal/versions", diff --git a/terraform-provider-constellation/internal/provider/cluster_resource.go b/terraform-provider-constellation/internal/provider/cluster_resource.go index 58432a396..2c5d97dd3 100644 --- a/terraform-provider-constellation/internal/provider/cluster_resource.go +++ b/terraform-provider-constellation/internal/provider/cluster_resource.go @@ -33,6 +33,7 @@ import ( "github.com/edgelesssys/constellation/v2/internal/constellation/state" "github.com/edgelesssys/constellation/v2/internal/grpc/dialer" "github.com/edgelesssys/constellation/v2/internal/kms/uri" + "github.com/edgelesssys/constellation/v2/internal/license" "github.com/edgelesssys/constellation/v2/internal/semver" "github.com/edgelesssys/constellation/v2/internal/versions" datastruct "github.com/edgelesssys/constellation/v2/terraform-provider-constellation/internal/data" @@ -82,6 +83,7 @@ type ClusterResourceModel struct { MasterSecretSalt types.String `tfsdk:"master_secret_salt"` MeasurementSalt types.String `tfsdk:"measurement_salt"` InitSecret types.String `tfsdk:"init_secret"` + LicenseID types.String `tfsdk:"license_id"` Attestation types.Object `tfsdk:"attestation"` GCP types.Object `tfsdk:"gcp"` Azure types.Object `tfsdk:"azure"` @@ -232,7 +234,14 @@ func (r *ClusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re Description: "Secret used for initialization of the cluster.", Required: true, }, + "license_id": schema.StringAttribute{ + MarkdownDescription: "Constellation license ID. When not set, the community license is used.", + Description: "Constellation license ID. When not set, the community license is used.", + Optional: true, + }, "attestation": newAttestationConfigAttributeSchema(attributeInput), + + // CSP specific inputs "gcp": schema.SingleNestedAttribute{ MarkdownDescription: "GCP-specific configuration.", Description: "GCP-specific configuration.", @@ -352,15 +361,36 @@ func (r *ClusterResource) Configure(_ context.Context, req resource.ConfigureReq } r.newApplier = func(ctx context.Context, validator atls.Validator) *constellation.Applier { - return constellation.NewApplier(&tfContextLogger{ctx: ctx}, &nopSpinner{}, newDialer) + return constellation.NewApplier(&tfContextLogger{ctx: ctx}, &nopSpinner{}, constellation.ApplyContextTerraform, newDialer) } } // ModifyPlan is called when the resource is planned for creation, updates, or deletion. This allows to set pre-apply // warnings and errors. func (r *ClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + if req.Plan.Raw.IsNull() { + return + } + + // Read plannedState supplied by Terraform runtime into the model + var plannedState ClusterResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plannedState)...) + if resp.Diagnostics.HasError() { + return + } + + licenseID := plannedState.LicenseID.ValueString() + if licenseID == "" { + resp.Diagnostics.AddWarning("Constellation license not found.", + "Using community license.\nFor details, see https://docs.edgeless.systems/constellation/overview/license") + } + if 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.Plan.Raw.IsNull() && !req.State.Raw.IsNull() { + if !req.State.Raw.IsNull() { // Read currentState supplied by Terraform runtime into the model var currentState ClusterResourceModel resp.Diagnostics.Append(req.State.Get(ctx, ¤tState)...) @@ -368,13 +398,6 @@ func (r *ClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPla return } - // Read plannedState supplied by Terraform runtime into the model - var plannedState ClusterResourceModel - resp.Diagnostics.Append(req.Plan.Get(ctx, &plannedState)...) - if resp.Diagnostics.HasError() { - return - } - // Warn the user about possibly destructive changes in case microservice changes are to be applied. currVer, diags := r.getMicroserviceVersion(ctx, ¤tState) resp.Diagnostics.Append(diags...) @@ -628,6 +651,17 @@ func (r *ClusterResource) apply(ctx context.Context, data *ClusterResourceModel, return diags } + // parse license ID + licenseID := data.LicenseID.ValueString() + if licenseID == "" { + licenseID = license.CommunityLicense + } + // license ID can be base64-encoded + licenseIDFromB64, err := base64.StdEncoding.DecodeString(licenseID) + if err == nil { + licenseID = string(licenseIDFromB64) + } + // Parse in-cluster service account info. serviceAccPayload := constellation.ServiceAccountPayload{} var gcpConfig gcpAttribute @@ -716,6 +750,16 @@ func (r *ClusterResource) apply(ctx context.Context, data *ClusterResourceModel, } } + // Check license + quota, err := applier.CheckLicense(ctx, csp, !skipInitRPC, licenseID) + if err != nil { + diags.AddWarning("Unable to contact license server.", "Please keep your vCPU quota in mind.") + } else if licenseID == license.CommunityLicense { + diags.AddWarning("Using community license.", "For details, see https://docs.edgeless.systems/constellation/overview/license") + } else { + tflog.Info(ctx, fmt.Sprintf("Please keep your vCPU quota (%d) in mind.", quota)) + } + // Now, we perform the actual applying. // Run init RPC