mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-03-02 03:40:07 -05: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)
|
# enable all tests (including integration)
|
||||||
test:integration --test_tag_filters= --@io_bazel_rules_go//go/config:tags=integration
|
test:integration --test_tag_filters= --@io_bazel_rules_go//go/config:tags=integration
|
||||||
# enable only integration tests
|
# 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
|
# bazel configs to explicitly target a platform
|
||||||
common:host --platforms @local_config_platform//:host
|
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{
|
apply := &applyCmd{
|
||||||
fileHandler: fileHandler,
|
fileHandler: fileHandler,
|
||||||
@ -824,7 +824,7 @@ type warnLog interface {
|
|||||||
// applier is used to run the different phases of the apply command.
|
// applier is used to run the different phases of the apply command.
|
||||||
type applier interface {
|
type applier interface {
|
||||||
SetKubeConfig(kubeConfig []byte) error
|
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"
|
// methods required by "init"
|
||||||
|
|
||||||
|
@ -536,7 +536,7 @@ type stubConstellApplier struct {
|
|||||||
|
|
||||||
func (s *stubConstellApplier) SetKubeConfig([]byte) error { return nil }
|
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
|
return 0, s.checkLicenseErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -369,7 +369,7 @@ func TestWriteOutput(t *testing.T) {
|
|||||||
spinner: &nopSpinner{},
|
spinner: &nopSpinner{},
|
||||||
merger: &stubMerger{},
|
merger: &stubMerger{},
|
||||||
log: logger.NewTest(t),
|
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)
|
err = i.writeInitOutput(stateFile, initOutput, false, &out, measurementSalt)
|
||||||
require.NoError(err)
|
require.NoError(err)
|
||||||
@ -461,7 +461,7 @@ func TestGenerateMasterSecret(t *testing.T) {
|
|||||||
i := &applyCmd{
|
i := &applyCmd{
|
||||||
fileHandler: fileHandler,
|
fileHandler: fileHandler,
|
||||||
log: logger.NewTest(t),
|
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)
|
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 {
|
if err != nil {
|
||||||
cmd.Printf("Unable to contact license server.\n")
|
cmd.Printf("Unable to contact license server.\n")
|
||||||
cmd.Printf("Please keep your vCPU quota in mind.\n")
|
cmd.Printf("Please keep your vCPU quota in mind.\n")
|
||||||
|
@ -20,6 +20,16 @@ import (
|
|||||||
"github.com/edgelesssys/constellation/v2/internal/license"
|
"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
|
// An Applier handles applying a specific configuration to a Constellation cluster
|
||||||
// with existing Infrastructure.
|
// with existing Infrastructure.
|
||||||
// In Particular, this involves Initialization and Upgrading of the cluster.
|
// In Particular, this involves Initialization and Upgrading of the cluster.
|
||||||
@ -28,6 +38,8 @@ type Applier struct {
|
|||||||
licenseChecker licenseChecker
|
licenseChecker licenseChecker
|
||||||
spinner spinnerInterf
|
spinner spinnerInterf
|
||||||
|
|
||||||
|
applyContext ApplyContext
|
||||||
|
|
||||||
// newDialer creates a new aTLS gRPC dialer.
|
// newDialer creates a new aTLS gRPC dialer.
|
||||||
newDialer func(validator atls.Validator) *dialer.Dialer
|
newDialer func(validator atls.Validator) *dialer.Dialer
|
||||||
kubecmdClient kubecmdClient
|
kubecmdClient kubecmdClient
|
||||||
@ -35,7 +47,7 @@ type Applier struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type licenseChecker interface {
|
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 {
|
type debugLog interface {
|
||||||
@ -44,14 +56,15 @@ type debugLog interface {
|
|||||||
|
|
||||||
// NewApplier creates a new Applier.
|
// NewApplier creates a new Applier.
|
||||||
func NewApplier(
|
func NewApplier(
|
||||||
log debugLog,
|
log debugLog, spinner spinnerInterf,
|
||||||
spinner spinnerInterf,
|
applyContext ApplyContext,
|
||||||
newDialer func(validator atls.Validator) *dialer.Dialer,
|
newDialer func(validator atls.Validator) *dialer.Dialer,
|
||||||
) *Applier {
|
) *Applier {
|
||||||
return &Applier{
|
return &Applier{
|
||||||
log: log,
|
log: log,
|
||||||
spinner: spinner,
|
spinner: spinner,
|
||||||
licenseChecker: license.NewChecker(license.NewClient()),
|
licenseChecker: license.NewChecker(),
|
||||||
|
applyContext: applyContext,
|
||||||
newDialer: newDialer,
|
newDialer: newDialer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,15 +86,26 @@ func (a *Applier) SetKubeConfig(kubeConfig []byte) error {
|
|||||||
|
|
||||||
// CheckLicense checks the given Constellation license with the license server
|
// CheckLicense checks the given Constellation license with the license server
|
||||||
// and returns the allowed quota for the license.
|
// 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)
|
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 {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("checking license: %w", err)
|
return 0, fmt.Errorf("checking license: %w", err)
|
||||||
}
|
}
|
||||||
a.log.Debugf("Got response from license server for license '%s'", licenseID)
|
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.
|
// GenerateMasterSecret generates a new master secret.
|
||||||
|
@ -38,7 +38,7 @@ func TestCheckLicense(t *testing.T) {
|
|||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
a := &Applier{licenseChecker: tc.licenseChecker, log: logger.NewTest(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 {
|
if tc.wantErr {
|
||||||
require.Error(err)
|
require.Error(err)
|
||||||
} else {
|
} else {
|
||||||
@ -52,8 +52,8 @@ type stubLicenseChecker struct {
|
|||||||
checkLicenseErr error
|
checkLicenseErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *stubLicenseChecker) CheckLicense(context.Context, cloudprovider.Provider, string) (license.QuotaCheckResponse, error) {
|
func (c *stubLicenseChecker) CheckLicense(context.Context, cloudprovider.Provider, license.Action, string) (int, error) {
|
||||||
return license.QuotaCheckResponse{}, c.checkLicenseErr
|
return 0, c.checkLicenseErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateMasterSecret(t *testing.T) {
|
func TestGenerateMasterSecret(t *testing.T) {
|
||||||
|
@ -22,11 +22,11 @@ go_library(
|
|||||||
|
|
||||||
go_test(
|
go_test(
|
||||||
name = "license_test",
|
name = "license_test",
|
||||||
srcs = [
|
srcs = ["file_test.go"],
|
||||||
"file_test.go",
|
|
||||||
"license_test.go",
|
|
||||||
],
|
|
||||||
embed = [":license"],
|
embed = [":license"],
|
||||||
|
tags = [
|
||||||
|
"enterprise",
|
||||||
|
],
|
||||||
deps = [
|
deps = [
|
||||||
"@com_github_stretchr_testify//assert",
|
"@com_github_stretchr_testify//assert",
|
||||||
"@com_github_stretchr_testify//require",
|
"@com_github_stretchr_testify//require",
|
||||||
|
@ -9,26 +9,90 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||||||
package license
|
package license
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
"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 {
|
type Checker struct {
|
||||||
quotaChecker QuotaChecker
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChecker(quotaChecker QuotaChecker) *Checker {
|
// NewChecker creates a new Checker.
|
||||||
|
func NewChecker() *Checker {
|
||||||
return &Checker{
|
return &Checker{
|
||||||
quotaChecker: quotaChecker,
|
httpClient: http.DefaultClient,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckLicense contacts the license server to fetch quota information for the given license.
|
// CheckLicense checks the Constellation license. If the license is valid, it returns the vCPU quota.
|
||||||
func (c *Checker) CheckLicense(ctx context.Context, provider cloudprovider.Provider, licenseID string) (QuotaCheckResponse, error) {
|
func (c *Checker) CheckLicense(ctx context.Context, csp cloudprovider.Provider, action Action, licenseID string) (int, error) {
|
||||||
return c.quotaChecker.QuotaCheck(ctx, QuotaCheckRequest{
|
checkRequest := quotaCheckRequest{
|
||||||
|
Provider: csp.String(),
|
||||||
License: licenseID,
|
License: licenseID,
|
||||||
Action: Init,
|
Action: action,
|
||||||
Provider: provider.String(),
|
}
|
||||||
})
|
|
||||||
|
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
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
|
||||||
@ -13,6 +15,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
"github.com/stretchr/testify/assert"
|
"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.
|
// newTestClient returns *http.Client with Transport replaced to avoid making real calls.
|
||||||
func newTestClient(fn roundTripFunc) *Client {
|
func newTestClient(fn roundTripFunc) *http.Client {
|
||||||
return &Client{
|
return &http.Client{
|
||||||
httpClient: &http.Client{
|
Transport: fn,
|
||||||
Transport: fn,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,31 +71,26 @@ func TestQuotaCheck(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
client := newTestClient(func(req *http.Request) *http.Response {
|
client := &Checker{
|
||||||
r := &http.Response{
|
httpClient: newTestClient(func(req *http.Request) *http.Response {
|
||||||
StatusCode: tc.serverResponseCode,
|
r := &http.Response{
|
||||||
Body: io.NopCloser(bytes.NewBufferString(tc.serverResponse)),
|
StatusCode: tc.serverResponseCode,
|
||||||
Header: make(http.Header),
|
Body: io.NopCloser(bytes.NewBufferString(tc.serverResponse)),
|
||||||
}
|
Header: make(http.Header),
|
||||||
r.Header.Set("Content-Type", tc.serverResponseContent)
|
}
|
||||||
return r
|
r.Header.Set("Content-Type", tc.serverResponseContent)
|
||||||
})
|
return r
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := client.QuotaCheck(context.Background(), QuotaCheckRequest{
|
quota, err := client.CheckLicense(context.Background(), cloudprovider.Unknown, Init, tc.license)
|
||||||
Action: test,
|
|
||||||
License: tc.license,
|
|
||||||
})
|
|
||||||
|
|
||||||
if tc.wantError {
|
if tc.wantError {
|
||||||
assert.Error(err)
|
assert.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
assert.NoError(err)
|
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{}
|
type Checker struct{}
|
||||||
|
|
||||||
// NewChecker creates a new Checker.
|
// NewChecker creates a new Checker.
|
||||||
func NewChecker(QuotaChecker) *Checker {
|
func NewChecker() *Checker {
|
||||||
return &Checker{}
|
return &Checker{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckLicense is a no-op for open source version of Constellation.
|
// CheckLicense is a no-op for open source version of Constellation.
|
||||||
func (c *Checker) CheckLicense(context.Context, cloudprovider.Provider, string) (QuotaCheckResponse, error) {
|
func (c *Checker) CheckLicense(context.Context, cloudprovider.Provider, Action, string) (int, error) {
|
||||||
return QuotaCheckResponse{}, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,12 @@ go_test(
|
|||||||
name = "integration_test",
|
name = "integration_test",
|
||||||
srcs = ["license_integration_test.go"],
|
srcs = ["license_integration_test.go"],
|
||||||
tags = [
|
tags = [
|
||||||
|
"enterprise",
|
||||||
"integration",
|
"integration",
|
||||||
"requires-network",
|
"requires-network",
|
||||||
],
|
],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//internal/cloud/cloudprovider",
|
||||||
"//internal/license",
|
"//internal/license",
|
||||||
"@com_github_stretchr_testify//assert",
|
"@com_github_stretchr_testify//assert",
|
||||||
],
|
],
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/license"
|
"github.com/edgelesssys/constellation/v2/internal/license"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -36,20 +37,16 @@ func TestQuotaCheckIntegration(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
|
|
||||||
client := license.NewClient()
|
client := license.NewChecker()
|
||||||
|
|
||||||
req := license.QuotaCheckRequest{
|
quota, err := client.CheckLicense(context.Background(), cloudprovider.Unknown, "test", tc.license)
|
||||||
Action: license.Action("test"),
|
|
||||||
License: tc.license,
|
|
||||||
}
|
|
||||||
resp, err := client.QuotaCheck(context.Background(), req)
|
|
||||||
|
|
||||||
if tc.wantError {
|
if tc.wantError {
|
||||||
assert.Error(err)
|
assert.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
assert.NoError(err)
|
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 provides functions to check a user's Constellation license.
|
||||||
package license
|
package license
|
||||||
|
|
||||||
import (
|
// Action performed by Constellation.
|
||||||
"bytes"
|
type Action string
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// CommunityLicense is used by everyone who has not bought an enterprise license.
|
// CommunityLicense is used by everyone who has not bought an enterprise license.
|
||||||
CommunityLicense = "00000000-0000-0000-0000-000000000000"
|
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 denotes the initialization of a Constellation cluster.
|
||||||
Init Action = "init"
|
Init Action = "init"
|
||||||
// test action is only to be used in testing.
|
// Apply action denotes an update of a Constellation cluster.
|
||||||
test Action = "test"
|
// 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))
|
- `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.
|
- `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].
|
- `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
|
### Read-Only
|
||||||
|
|
||||||
|
@ -33,6 +33,7 @@ go_library(
|
|||||||
"//internal/grpc/dialer",
|
"//internal/grpc/dialer",
|
||||||
"//internal/imagefetcher",
|
"//internal/imagefetcher",
|
||||||
"//internal/kms/uri",
|
"//internal/kms/uri",
|
||||||
|
"//internal/license",
|
||||||
"//internal/semver",
|
"//internal/semver",
|
||||||
"//internal/sigstore",
|
"//internal/sigstore",
|
||||||
"//internal/versions",
|
"//internal/versions",
|
||||||
|
@ -35,6 +35,7 @@ import (
|
|||||||
"github.com/edgelesssys/constellation/v2/internal/constellation/state"
|
"github.com/edgelesssys/constellation/v2/internal/constellation/state"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/grpc/dialer"
|
"github.com/edgelesssys/constellation/v2/internal/grpc/dialer"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/kms/uri"
|
"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/semver"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/versions"
|
"github.com/edgelesssys/constellation/v2/internal/versions"
|
||||||
datastruct "github.com/edgelesssys/constellation/v2/terraform-provider-constellation/internal/data"
|
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"`
|
MasterSecretSalt types.String `tfsdk:"master_secret_salt"`
|
||||||
MeasurementSalt types.String `tfsdk:"measurement_salt"`
|
MeasurementSalt types.String `tfsdk:"measurement_salt"`
|
||||||
InitSecret types.String `tfsdk:"init_secret"`
|
InitSecret types.String `tfsdk:"init_secret"`
|
||||||
|
LicenseID types.String `tfsdk:"license_id"`
|
||||||
Attestation types.Object `tfsdk:"attestation"`
|
Attestation types.Object `tfsdk:"attestation"`
|
||||||
GCP types.Object `tfsdk:"gcp"`
|
GCP types.Object `tfsdk:"gcp"`
|
||||||
Azure types.Object `tfsdk:"azure"`
|
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.",
|
Description: "Secret used for initialization of the cluster.",
|
||||||
Required: true,
|
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),
|
"attestation": newAttestationConfigAttributeSchema(attributeInput),
|
||||||
|
|
||||||
|
// CSP specific inputs
|
||||||
"gcp": schema.SingleNestedAttribute{
|
"gcp": schema.SingleNestedAttribute{
|
||||||
MarkdownDescription: "GCP-specific configuration.",
|
MarkdownDescription: "GCP-specific configuration.",
|
||||||
Description: "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 {
|
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
|
// ModifyPlan is called when the resource is planned for creation, updates, or deletion. This allows to set pre-apply
|
||||||
// warnings and errors.
|
// warnings and errors.
|
||||||
func (r *ClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
|
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)
|
// 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
|
// Read currentState supplied by Terraform runtime into the model
|
||||||
var currentState ClusterResourceModel
|
var currentState ClusterResourceModel
|
||||||
resp.Diagnostics.Append(req.State.Get(ctx, ¤tState)...)
|
resp.Diagnostics.Append(req.State.Get(ctx, ¤tState)...)
|
||||||
@ -458,13 +488,6 @@ func (r *ClusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPla
|
|||||||
return
|
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.
|
// Warn the user about possibly destructive changes in case microservice changes are to be applied.
|
||||||
currVer, diags := r.getMicroserviceVersion(ctx, ¤tState)
|
currVer, diags := r.getMicroserviceVersion(ctx, ¤tState)
|
||||||
resp.Diagnostics.Append(diags...)
|
resp.Diagnostics.Append(diags...)
|
||||||
@ -714,6 +737,17 @@ func (r *ClusterResource) apply(ctx context.Context, data *ClusterResourceModel,
|
|||||||
return diags
|
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.
|
// Parse in-cluster service account info.
|
||||||
serviceAccPayload := constellation.ServiceAccountPayload{}
|
serviceAccPayload := constellation.ServiceAccountPayload{}
|
||||||
var gcpConfig gcpAttribute
|
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.
|
// Now, we perform the actual applying.
|
||||||
|
|
||||||
// Run init RPC
|
// Run init RPC
|
||||||
|
Loading…
x
Reference in New Issue
Block a user