AB#2360 enterprise build tag (#397)

* enterprise build switch to disable license checking in default (OSS) version
* remove community license quota
* empty image references on OSS build in config
Signed-off-by: Fabian Kammel <fk@edgeless.systems>
This commit is contained in:
Fabian Kammel 2022-08-25 14:06:29 +02:00 committed by GitHub
parent 6b1c20792a
commit 45beec15f5
13 changed files with 126 additions and 58 deletions

View File

@ -51,7 +51,7 @@ runs:
GIT_TAG=$(git describe --tags --always --dirty --abbrev=0) GIT_TAG=$(git describe --tags --always --dirty --abbrev=0)
mkdir -p build mkdir -p build
cd build cd build
cmake -DCLI_VERSION:STRING=${GIT_TAG} .. cmake -DCLI_BUILD_TAGS:STRING=gcp,enterprise -DCLI_VERSION:STRING=${GIT_TAG} ..
GOOS=${{ inputs.targetOS }} GOARCH=${{ inputs.targetArch }} make -j`nproc` cli GOOS=${{ inputs.targetOS }} GOARCH=${{ inputs.targetArch }} make -j`nproc` cli
cp constellation constellation-${{ inputs.targetOS }}-${{ inputs.targetArch }} cp constellation constellation-${{ inputs.targetOS }}-${{ inputs.targetArch }}
echo "$(pwd)" >> $GITHUB_PATH echo "$(pwd)" >> $GITHUB_PATH

View File

@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.11)
project(constellation LANGUAGES C VERSION 0.1.0) project(constellation LANGUAGES C VERSION 0.1.0)
set(CLI_VERSION "v0.1.0" CACHE STRING "Version of CLI binary.") set(CLI_VERSION "v0.1.0" CACHE STRING "Version of CLI binary.")
set(CLI_BUILD_TAGS "gcp" CACHE STRING "Tags passed to go build of Constellation CLI.")
enable_testing() enable_testing()
@ -30,7 +31,7 @@ add_custom_target(bootstrapper ALL
# #
add_custom_target(cli ALL add_custom_target(cli ALL
CGO_ENABLED=0 go build -o ${CMAKE_BINARY_DIR}/constellation -tags=gcp -ldflags "-buildid='' -X github.com/edgelesssys/constellation/internal/constants.VersionInfo=${CLI_VERSION}" CGO_ENABLED=0 go build -o ${CMAKE_BINARY_DIR}/constellation -tags='${CLI_BUILD_TAGS}' -ldflags "-buildid='' -X github.com/edgelesssys/constellation/internal/constants.VersionInfo=${CLI_VERSION}"
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/cli WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/cli
BYPRODUCTS constellation BYPRODUCTS constellation
) )

View File

@ -65,7 +65,7 @@ func runInitialize(cmd *cobra.Command, args []string) error {
// initialize initializes a Constellation. // initialize initializes a Constellation.
func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator) *dialer.Dialer, func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator) *dialer.Dialer,
serviceAccCreator serviceAccountCreator, fileHandler file.Handler, helmLoader helmLoader, licenseClient licenseClient, serviceAccCreator serviceAccountCreator, fileHandler file.Handler, helmLoader helmLoader, quotaChecker license.QuotaChecker,
) error { ) error {
flags, err := evalFlagArgs(cmd, fileHandler) flags, err := evalFlagArgs(cmd, fileHandler)
if err != nil { if err != nil {
@ -87,22 +87,10 @@ func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator
return fmt.Errorf("reading and validating config: %w", err) return fmt.Errorf("reading and validating config: %w", err)
} }
licenseID, err := license.FromFile(fileHandler, constants.LicenseFilename) checker := license.NewChecker(quotaChecker, fileHandler)
if err != nil { if err := checker.CheckLicense(cmd.Context(), cmd.Printf); err != nil {
cmd.Println("Unable to find license file. Assuming community license.") cmd.Printf("License check failed: %v", err)
licenseID = license.CommunityLicense
} }
quotaResp, err := licenseClient.CheckQuota(cmd.Context(), license.CheckQuotaRequest{
License: licenseID,
Action: license.Init,
})
if err != nil {
cmd.Println("Unable to contact license server.")
cmd.Println("Please keep your vCPU quota in mind.")
cmd.Printf("For community installation the vCPU quota is: %d.\n", license.CommunityQuota)
}
cmd.Printf("Constellation license found: %s\n", licenseID)
cmd.Printf("Please keep your vCPU quota (%d) in mind.\n", quotaResp.Quota)
var sshUsers []*ssh.UserKey var sshUsers []*ssh.UserKey
for _, user := range config.SSHUsers { for _, user := range config.SSHUsers {
@ -414,7 +402,3 @@ func initCompletion(cmd *cobra.Command, args []string, toComplete string) ([]str
type grpcDialer interface { type grpcDialer interface {
Dial(ctx context.Context, target string) (*grpc.ClientConn, error) Dial(ctx context.Context, target string) (*grpc.ClientConn, error)
} }
type licenseClient interface {
CheckQuota(ctx context.Context, checkRequest license.CheckQuotaRequest) (license.CheckQuotaResponse, error)
}

View File

@ -547,11 +547,13 @@ func defaultConfigWithExpectedMeasurements(t *testing.T, conf *config.Config, cs
conf.Provider.Azure.TenantID = "01234567-0123-0123-0123-0123456789ab" conf.Provider.Azure.TenantID = "01234567-0123-0123-0123-0123456789ab"
conf.Provider.Azure.Location = "test-location" conf.Provider.Azure.Location = "test-location"
conf.Provider.Azure.UserAssignedIdentity = "test-identity" conf.Provider.Azure.UserAssignedIdentity = "test-identity"
conf.Provider.Azure.Image = "some/image/location"
conf.Provider.Azure.Measurements[8] = []byte("00000000000000000000000000000000") conf.Provider.Azure.Measurements[8] = []byte("00000000000000000000000000000000")
conf.Provider.Azure.Measurements[9] = []byte("11111111111111111111111111111111") conf.Provider.Azure.Measurements[9] = []byte("11111111111111111111111111111111")
case cloudprovider.GCP: case cloudprovider.GCP:
conf.Provider.GCP.Region = "test-region" conf.Provider.GCP.Region = "test-region"
conf.Provider.GCP.Project = "test-project" conf.Provider.GCP.Project = "test-project"
conf.Provider.GCP.Image = "some/image/location"
conf.Provider.GCP.Zone = "test-zone" conf.Provider.GCP.Zone = "test-zone"
conf.Provider.GCP.Measurements[8] = []byte("00000000000000000000000000000000") conf.Provider.GCP.Measurements[8] = []byte("00000000000000000000000000000000")
conf.Provider.GCP.Measurements[9] = []byte("11111111111111111111111111111111") conf.Provider.GCP.Measurements[9] = []byte("11111111111111111111111111111111")
@ -566,8 +568,8 @@ func defaultConfigWithExpectedMeasurements(t *testing.T, conf *config.Config, cs
type stubLicenseClient struct{} type stubLicenseClient struct{}
func (c *stubLicenseClient) CheckQuota(ctx context.Context, checkRequest license.CheckQuotaRequest) (license.CheckQuotaResponse, error) { func (c *stubLicenseClient) QuotaCheck(ctx context.Context, checkRequest license.QuotaCheckRequest) (license.QuotaCheckResponse, error) {
return license.CheckQuotaResponse{ return license.QuotaCheckResponse{
Quota: license.CommunityQuota, Quota: 25,
}, nil }, nil
} }

View File

@ -226,7 +226,7 @@ func Default() *Config {
TenantID: "", TenantID: "",
Location: "", Location: "",
UserAssignedIdentity: "", UserAssignedIdentity: "",
Image: "/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/CONSTELLATION-IMAGES/providers/Microsoft.Compute/galleries/Constellation/images/constellation/versions/1.5.0", Image: DefaultImageAzure,
StateDiskType: "StandardSSD_LRS", // TODO: Replace with Premium_LRS when we replace the default VM size (Standard_D2a_v4) since the size does not support Premium_LRS StateDiskType: "StandardSSD_LRS", // TODO: Replace with Premium_LRS when we replace the default VM size (Standard_D2a_v4) since the size does not support Premium_LRS
Measurements: copyPCRMap(azurePCRs), Measurements: copyPCRMap(azurePCRs),
EnforcedMeasurements: []uint32{8, 9, 11, 12}, EnforcedMeasurements: []uint32{8, 9, 11, 12},
@ -235,7 +235,7 @@ func Default() *Config {
Project: "", Project: "",
Region: "", Region: "",
Zone: "", Zone: "",
Image: "projects/constellation-images/global/images/constellation-v1-5-0", Image: DefaultImageGCP,
StateDiskType: "pd-ssd", StateDiskType: "pd-ssd",
ServiceAccountKeyPath: "serviceAccountKey.json", ServiceAccountKeyPath: "serviceAccountKey.json",
Measurements: copyPCRMap(gcpPCRs), Measurements: copyPCRMap(gcpPCRs),

View File

@ -13,6 +13,8 @@ import (
"go.uber.org/goleak" "go.uber.org/goleak"
) )
const defaultMsgCount = 9 // expect this number of error messages by default because user-specific values are not set
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
goleak.VerifyTestMain(m) goleak.VerifyTestMain(m)
} }
@ -154,7 +156,7 @@ func TestValidate(t *testing.T) {
}{ }{
"default config is valid": { "default config is valid": {
cnf: Default(), cnf: Default(),
wantMsgCount: 7, // expect 7 error messages by default because user-specific values are not set wantMsgCount: defaultMsgCount,
}, },
"config with 1 error": { "config with 1 error": {
cnf: func() *Config { cnf: func() *Config {
@ -162,7 +164,7 @@ func TestValidate(t *testing.T) {
cnf.Version = "v0" cnf.Version = "v0"
return cnf return cnf
}(), }(),
wantMsgCount: 8, wantMsgCount: defaultMsgCount + 1,
}, },
"config with 2 errors": { "config with 2 errors": {
cnf: func() *Config { cnf: func() *Config {
@ -171,7 +173,7 @@ func TestValidate(t *testing.T) {
cnf.StateDiskSizeGB = -1 cnf.StateDiskSizeGB = -1
return cnf return cnf
}(), }(),
wantMsgCount: 9, wantMsgCount: defaultMsgCount + 2,
}, },
} }

View File

@ -0,0 +1,8 @@
//go:build enterprise
package config
const (
DefaultImageAzure = "/subscriptions/0d202bbb-4fa7-4af8-8125-58c269a05435/resourceGroups/CONSTELLATION-IMAGES/providers/Microsoft.Compute/galleries/Constellation/images/constellation/versions/1.5.0"
DefaultImageGCP = "projects/constellation-images/global/images/constellation-v1-5-0"
)

View File

@ -0,0 +1,8 @@
//go:build !enterprise
package config
const (
DefaultImageAzure = ""
DefaultImageGCP = ""
)

View File

@ -0,0 +1,46 @@
//go:build enterprise
package license
import (
"context"
"github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/internal/file"
)
type Checker struct {
quotaChecker QuotaChecker
fileHandler file.Handler
}
func NewChecker(quotaChecker QuotaChecker, fileHandler file.Handler) *Checker {
return &Checker{
quotaChecker: quotaChecker,
fileHandler: fileHandler,
}
}
// CheckLicense tries to read the license file and contact license server
// to fetch quota information.
// If no license file is found, community license is assumed.
func (c *Checker) CheckLicense(ctx context.Context, printer func(string, ...any)) error {
licenseID, err := FromFile(c.fileHandler, constants.LicenseFilename)
if err != nil {
printer("Unable to find license file. Assuming community license.\n")
licenseID = CommunityLicense
} else {
printer("Constellation license found!\n")
}
quotaResp, err := c.quotaChecker.QuotaCheck(ctx, QuotaCheckRequest{
License: licenseID,
Action: Init,
})
if err != nil {
printer("Unable to contact license server.\n")
printer("Please keep your vCPU quota in mind.\n")
} else {
printer("Please keep your vCPU quota (%d) in mind.\n", quotaResp.Quota)
}
return nil
}

View File

@ -0,0 +1,20 @@
//go:build !enterprise
package license
import (
"context"
"github.com/edgelesssys/constellation/internal/file"
)
type Checker struct{}
func NewChecker(quotaChecker QuotaChecker, fileHandler file.Handler) *Checker {
return &Checker{}
}
// CheckLicense is a no-op for open source version of Constellation.
func (c *Checker) CheckLicense(ctx context.Context, printer func(string, ...any)) error {
return nil
}

View File

@ -12,10 +12,8 @@ import (
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"
// CommunityQuota is the vCPU quota allowed for community installations of Constellation. apiHost = "license.confidential.cloud"
CommunityQuota = 8 licensePath = "api/v1/license"
apiHost = "license.confidential.cloud"
licensePath = "api/v1/license"
) )
type Action string type Action string
@ -37,46 +35,46 @@ func NewClient() *Client {
} }
} }
// CheckQuotaRequest is JSON request to license server to check quota for a given license and action. // QuotaCheckRequest is JSON request to license server to check quota for a given license and action.
type CheckQuotaRequest struct { type QuotaCheckRequest struct {
Action Action `json:"action"` Action Action `json:"action"`
License string `json:"license"` License string `json:"license"`
} }
// CheckQuotaResponse is JSON response by license server. // QuotaCheckResponse is JSON response by license server.
type CheckQuotaResponse struct { type QuotaCheckResponse struct {
Quota int `json:"quota"` Quota int `json:"quota"`
} }
// CheckQuota for a given license and action, passed via CheckQuotaRequest. // QuotaCheck for a given license and action, passed via CheckQuotaRequest.
func (c *Client) CheckQuota(ctx context.Context, checkRequest CheckQuotaRequest) (CheckQuotaResponse, error) { func (c *Client) QuotaCheck(ctx context.Context, checkRequest QuotaCheckRequest) (QuotaCheckResponse, error) {
reqBody, err := json.Marshal(checkRequest) reqBody, err := json.Marshal(checkRequest)
if err != nil { if err != nil {
return CheckQuotaResponse{}, fmt.Errorf("unable to marshal input: %w", err) return QuotaCheckResponse{}, fmt.Errorf("unable to marshal input: %w", err)
} }
req, err := http.NewRequestWithContext(ctx, http.MethodPost, licenseURL().String(), bytes.NewBuffer(reqBody)) req, err := http.NewRequestWithContext(ctx, http.MethodPost, licenseURL().String(), bytes.NewBuffer(reqBody))
if err != nil { if err != nil {
return CheckQuotaResponse{}, fmt.Errorf("unable to create request: %w", err) return QuotaCheckResponse{}, fmt.Errorf("unable to create request: %w", err)
} }
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
if err != nil { if err != nil {
return CheckQuotaResponse{}, fmt.Errorf("unable to do request: %w", err) return QuotaCheckResponse{}, fmt.Errorf("unable to do request: %w", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return CheckQuotaResponse{}, fmt.Errorf("http error %d", resp.StatusCode) return QuotaCheckResponse{}, fmt.Errorf("http error %d", resp.StatusCode)
} }
responseContentType := resp.Header.Get("Content-Type") responseContentType := resp.Header.Get("Content-Type")
if responseContentType != "application/json" { if responseContentType != "application/json" {
return CheckQuotaResponse{}, fmt.Errorf("expected server JSON response but got '%s'", responseContentType) return QuotaCheckResponse{}, fmt.Errorf("expected server JSON response but got '%s'", responseContentType)
} }
var parsedResponse CheckQuotaResponse var parsedResponse QuotaCheckResponse
err = json.NewDecoder(resp.Body).Decode(&parsedResponse) err = json.NewDecoder(resp.Body).Decode(&parsedResponse)
if err != nil { if err != nil {
return CheckQuotaResponse{}, fmt.Errorf("unable to parse response: %w", err) return QuotaCheckResponse{}, fmt.Errorf("unable to parse response: %w", err)
} }
return parsedResponse, nil return parsedResponse, nil
@ -89,3 +87,7 @@ func licenseURL() *url.URL {
Path: licensePath, Path: licensePath,
} }
} }
type QuotaChecker interface {
QuotaCheck(ctx context.Context, checkRequest QuotaCheckRequest) (QuotaCheckResponse, error)
}

View File

@ -9,18 +9,13 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestCheckQuotaIntegration(t *testing.T) { func TestQuotaCheckIntegration(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
license string license string
action Action action Action
wantQuota int wantQuota int
wantError bool wantError bool
}{ }{
"ES license has quota 256": {
license: "***REMOVED***",
action: test,
wantQuota: 256,
},
"OSS license has quota 8": { "OSS license has quota 8": {
license: CommunityLicense, license: CommunityLicense,
action: test, action: test,
@ -49,11 +44,11 @@ func TestCheckQuotaIntegration(t *testing.T) {
client := NewClient() client := NewClient()
req := CheckQuotaRequest{ req := QuotaCheckRequest{
Action: tc.action, Action: tc.action,
License: tc.license, License: tc.license,
} }
resp, err := client.CheckQuota(context.Background(), req) resp, err := client.QuotaCheck(context.Background(), req)
if tc.wantError { if tc.wantError {
assert.Error(err) assert.Error(err)

View File

@ -27,7 +27,7 @@ func newTestClient(fn roundTripFunc) *Client {
} }
} }
func TestCheckQuota(t *testing.T) { func TestQuotaCheck(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
license string license string
serverResponse string serverResponse string
@ -37,7 +37,7 @@ func TestCheckQuota(t *testing.T) {
wantError bool wantError bool
}{ }{
"success": { "success": {
license: "***REMOVED***", license: "0c0a6558-f8af-4063-bf61-92e7ac4cb052",
serverResponse: "{\"quota\":256}", serverResponse: "{\"quota\":256}",
serverResponseCode: http.StatusOK, serverResponseCode: http.StatusOK,
serverResponseContent: "application/json", serverResponseContent: "application/json",
@ -74,7 +74,7 @@ func TestCheckQuota(t *testing.T) {
return r return r
}) })
resp, err := client.CheckQuota(context.Background(), CheckQuotaRequest{ resp, err := client.QuotaCheck(context.Background(), QuotaCheckRequest{
Action: test, Action: test,
License: tc.license, License: tc.license,
}) })