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)
mkdir -p 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
cp constellation constellation-${{ inputs.targetOS }}-${{ inputs.targetArch }}
echo "$(pwd)" >> $GITHUB_PATH

View File

@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 3.11)
project(constellation LANGUAGES C VERSION 0.1.0)
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()
@ -30,7 +31,7 @@ add_custom_target(bootstrapper 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
BYPRODUCTS constellation
)

View File

@ -65,7 +65,7 @@ func runInitialize(cmd *cobra.Command, args []string) error {
// initialize initializes a Constellation.
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 {
flags, err := evalFlagArgs(cmd, fileHandler)
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)
}
licenseID, err := license.FromFile(fileHandler, constants.LicenseFilename)
if err != nil {
cmd.Println("Unable to find license file. Assuming community license.")
licenseID = license.CommunityLicense
checker := license.NewChecker(quotaChecker, fileHandler)
if err := checker.CheckLicense(cmd.Context(), cmd.Printf); err != nil {
cmd.Printf("License check failed: %v", err)
}
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
for _, user := range config.SSHUsers {
@ -414,7 +402,3 @@ func initCompletion(cmd *cobra.Command, args []string, toComplete string) ([]str
type grpcDialer interface {
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.Location = "test-location"
conf.Provider.Azure.UserAssignedIdentity = "test-identity"
conf.Provider.Azure.Image = "some/image/location"
conf.Provider.Azure.Measurements[8] = []byte("00000000000000000000000000000000")
conf.Provider.Azure.Measurements[9] = []byte("11111111111111111111111111111111")
case cloudprovider.GCP:
conf.Provider.GCP.Region = "test-region"
conf.Provider.GCP.Project = "test-project"
conf.Provider.GCP.Image = "some/image/location"
conf.Provider.GCP.Zone = "test-zone"
conf.Provider.GCP.Measurements[8] = []byte("00000000000000000000000000000000")
conf.Provider.GCP.Measurements[9] = []byte("11111111111111111111111111111111")
@ -566,8 +568,8 @@ func defaultConfigWithExpectedMeasurements(t *testing.T, conf *config.Config, cs
type stubLicenseClient struct{}
func (c *stubLicenseClient) CheckQuota(ctx context.Context, checkRequest license.CheckQuotaRequest) (license.CheckQuotaResponse, error) {
return license.CheckQuotaResponse{
Quota: license.CommunityQuota,
func (c *stubLicenseClient) QuotaCheck(ctx context.Context, checkRequest license.QuotaCheckRequest) (license.QuotaCheckResponse, error) {
return license.QuotaCheckResponse{
Quota: 25,
}, nil
}

View File

@ -226,7 +226,7 @@ func Default() *Config {
TenantID: "",
Location: "",
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
Measurements: copyPCRMap(azurePCRs),
EnforcedMeasurements: []uint32{8, 9, 11, 12},
@ -235,7 +235,7 @@ func Default() *Config {
Project: "",
Region: "",
Zone: "",
Image: "projects/constellation-images/global/images/constellation-v1-5-0",
Image: DefaultImageGCP,
StateDiskType: "pd-ssd",
ServiceAccountKeyPath: "serviceAccountKey.json",
Measurements: copyPCRMap(gcpPCRs),

View File

@ -13,6 +13,8 @@ import (
"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) {
goleak.VerifyTestMain(m)
}
@ -154,7 +156,7 @@ func TestValidate(t *testing.T) {
}{
"default config is valid": {
cnf: Default(),
wantMsgCount: 7, // expect 7 error messages by default because user-specific values are not set
wantMsgCount: defaultMsgCount,
},
"config with 1 error": {
cnf: func() *Config {
@ -162,7 +164,7 @@ func TestValidate(t *testing.T) {
cnf.Version = "v0"
return cnf
}(),
wantMsgCount: 8,
wantMsgCount: defaultMsgCount + 1,
},
"config with 2 errors": {
cnf: func() *Config {
@ -171,7 +173,7 @@ func TestValidate(t *testing.T) {
cnf.StateDiskSizeGB = -1
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 (
// CommunityLicense is used by everyone who has not bought an enterprise license.
CommunityLicense = "00000000-0000-0000-0000-000000000000"
// CommunityQuota is the vCPU quota allowed for community installations of Constellation.
CommunityQuota = 8
apiHost = "license.confidential.cloud"
licensePath = "api/v1/license"
apiHost = "license.confidential.cloud"
licensePath = "api/v1/license"
)
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.
type CheckQuotaRequest struct {
// QuotaCheckRequest is JSON request to license server to check quota for a given license and action.
type QuotaCheckRequest struct {
Action Action `json:"action"`
License string `json:"license"`
}
// CheckQuotaResponse is JSON response by license server.
type CheckQuotaResponse struct {
// QuotaCheckResponse is JSON response by license server.
type QuotaCheckResponse struct {
Quota int `json:"quota"`
}
// CheckQuota for a given license and action, passed via CheckQuotaRequest.
func (c *Client) CheckQuota(ctx context.Context, checkRequest CheckQuotaRequest) (CheckQuotaResponse, error) {
// 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 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))
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)
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()
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")
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)
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
@ -89,3 +87,7 @@ func licenseURL() *url.URL {
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"
)
func TestCheckQuotaIntegration(t *testing.T) {
func TestQuotaCheckIntegration(t *testing.T) {
testCases := map[string]struct {
license string
action Action
wantQuota int
wantError bool
}{
"ES license has quota 256": {
license: "***REMOVED***",
action: test,
wantQuota: 256,
},
"OSS license has quota 8": {
license: CommunityLicense,
action: test,
@ -49,11 +44,11 @@ func TestCheckQuotaIntegration(t *testing.T) {
client := NewClient()
req := CheckQuotaRequest{
req := QuotaCheckRequest{
Action: tc.action,
License: tc.license,
}
resp, err := client.CheckQuota(context.Background(), req)
resp, err := client.QuotaCheck(context.Background(), req)
if tc.wantError {
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 {
license string
serverResponse string
@ -37,7 +37,7 @@ func TestCheckQuota(t *testing.T) {
wantError bool
}{
"success": {
license: "***REMOVED***",
license: "0c0a6558-f8af-4063-bf61-92e7ac4cb052",
serverResponse: "{\"quota\":256}",
serverResponseCode: http.StatusOK,
serverResponseContent: "application/json",
@ -74,7 +74,7 @@ func TestCheckQuota(t *testing.T) {
return r
})
resp, err := client.CheckQuota(context.Background(), CheckQuotaRequest{
resp, err := client.QuotaCheck(context.Background(), QuotaCheckRequest{
Action: test,
License: tc.license,
})