mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
constellation-lib: run license check in Terraform provider and refactor code (#2740)
* Clean up license checker code Signed-off-by: Daniel Weiße <dw@edgeless.systems> * Create license check depending on init/upgrade actions Signed-off-by: Daniel Weiße <dw@edgeless.systems> * Run license check in Terraform provider Signed-off-by: Daniel Weiße <dw@edgeless.systems> * fix license integration test action Signed-off-by: Daniel Weiße <dw@edgeless.systems> * Run tests with enterprise tag Signed-off-by: Daniel Weiße <dw@edgeless.systems> * Allow b64 encoding for license ID Signed-off-by: Daniel Weiße <dw@edgeless.systems> * Update checker_enterprise.go --------- Signed-off-by: Daniel Weiße <dw@edgeless.systems> Co-authored-by: Thomas Tendyck <51411342+thomasten@users.noreply.github.com>
This commit is contained in:
parent
ac1f322044
commit
519efe637d
2
.bazelrc
2
.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
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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.
|
||||
|
@ -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) {
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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",
|
||||
],
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ See the [full list of CSPs](https://docs.edgeless.systems/constellation/overview
|
||||
- `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
|
||||
|
||||
|
@ -33,6 +33,7 @@ go_library(
|
||||
"//internal/grpc/dialer",
|
||||
"//internal/imagefetcher",
|
||||
"//internal/kms/uri",
|
||||
"//internal/license",
|
||||
"//internal/semver",
|
||||
"//internal/sigstore",
|
||||
"//internal/versions",
|
||||
|
@ -35,6 +35,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"
|
||||
@ -91,6 +92,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"`
|
||||
@ -258,7 +260,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.",
|
||||
@ -442,15 +451,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)...)
|
||||
@ -458,13 +488,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...)
|
||||
@ -714,6 +737,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
|
||||
@ -802,6 +836,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
|
||||
|
Loading…
Reference in New Issue
Block a user