Manually manage GCP service accounts

This commit is contained in:
katexochen 2022-08-23 17:49:55 +02:00 committed by Paul Meyer
parent f9c70d5c5a
commit e761c9bf97
19 changed files with 186 additions and 555 deletions

View File

@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Create multiple load balancers to enable load balacing TCP traffic for different backend services. All load balancers currently share the same public IP address.
- Improve rollback on GCP resource termination. You can now terminate multiple times.
- Implement SSH peer to peer distribution between debugd nodes.
- GCP service account can now be managed manually.
### Changed
<!-- For changes in existing functionality. -->

View File

@ -15,12 +15,10 @@ type gcpclient interface {
CreateFirewall(ctx context.Context, input gcpcl.FirewallInput) error
CreateInstances(ctx context.Context, input gcpcl.CreateInstancesInput) error
CreateLoadBalancers(ctx context.Context) error
CreateServiceAccount(ctx context.Context, input gcpcl.ServiceAccountInput) (string, error)
TerminateFirewall(ctx context.Context) error
TerminateVPCs(context.Context) error
TerminateLoadBalancers(context.Context) error
TerminateInstances(context.Context) error
TerminateServiceAccount(ctx context.Context) error
Close() error
}

View File

@ -11,7 +11,6 @@ import (
"github.com/edgelesssys/constellation/internal/azureshared"
"github.com/edgelesssys/constellation/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/internal/cloud/cloudtypes"
"github.com/edgelesssys/constellation/internal/gcpshared"
"github.com/edgelesssys/constellation/internal/state"
"go.uber.org/goleak"
)
@ -244,7 +243,6 @@ type fakeGcpClient struct {
uid string
name string
zone string
serviceAccount string
loadbalancers []string
}
@ -264,7 +262,6 @@ func (c *fakeGcpClient) GetState() state.ConstellationState {
Name: c.name,
UID: c.uid,
GCPZone: c.zone,
GCPServiceAccount: c.serviceAccount,
GCPLoadbalancers: c.loadbalancers,
}
}
@ -283,7 +280,6 @@ func (c *fakeGcpClient) SetState(stat state.ConstellationState) {
c.name = stat.Name
c.uid = stat.UID
c.zone = stat.GCPZone
c.serviceAccount = stat.GCPServiceAccount
c.loadbalancers = stat.GCPLoadbalancers
}
@ -321,22 +317,6 @@ func (c *fakeGcpClient) CreateInstances(ctx context.Context, input gcpcl.CreateI
return nil
}
func (c *fakeGcpClient) CreateServiceAccount(ctx context.Context, input gcpcl.ServiceAccountInput) (string, error) {
c.serviceAccount = "service-account@" + c.project + ".iam.gserviceaccount.com"
return gcpshared.ServiceAccountKey{
Type: "service_account",
ProjectID: c.project,
PrivateKeyID: "key-id",
PrivateKey: "-----BEGIN PRIVATE KEY-----\nprivate-key\n-----END PRIVATE KEY-----\n",
ClientEmail: c.serviceAccount,
ClientID: "client-id",
AuthURI: "https://accounts.google.com/o/oauth2/auth",
TokenURI: "https://accounts.google.com/o/oauth2/token",
AuthProviderX509CertURL: "https://www.googleapis.com/oauth2/v1/certs",
ClientX509CertURL: "https://www.googleapis.com/robot/v1/metadata/x509/service-account-email",
}.ToCloudServiceAccountURI(), nil
}
func (c *fakeGcpClient) CreateLoadBalancers(ctx context.Context) error {
c.loadbalancers = []string{"kube-lb", "boot-lb", "verify-lb"}
return nil
@ -369,11 +349,6 @@ func (c *fakeGcpClient) TerminateInstances(context.Context) error {
return nil
}
func (c *fakeGcpClient) TerminateServiceAccount(context.Context) error {
c.serviceAccount = ""
return nil
}
func (c *fakeGcpClient) TerminateLoadBalancers(context.Context) error {
c.loadbalancers = nil
return nil
@ -384,23 +359,20 @@ func (c *fakeGcpClient) Close() error {
}
type stubGcpClient struct {
terminateFirewallCalled bool
terminateInstancesCalled bool
terminateVPCsCalled bool
terminateServiceAccountCalled bool
closeCalled bool
terminateFirewallCalled bool
terminateInstancesCalled bool
terminateVPCsCalled bool
closeCalled bool
createVPCsErr error
createFirewallErr error
createInstancesErr error
createServiceAccountErr error
createLoadBalancerErr error
terminateFirewallErr error
terminateVPCsErr error
terminateInstancesErr error
terminateServiceAccountErr error
terminateLoadBalancerErr error
closeErr error
createVPCsErr error
createFirewallErr error
createInstancesErr error
createLoadBalancerErr error
terminateFirewallErr error
terminateVPCsErr error
terminateInstancesErr error
terminateLoadBalancerErr error
closeErr error
}
func (c *stubGcpClient) GetState() state.ConstellationState {
@ -422,10 +394,6 @@ func (c *stubGcpClient) CreateInstances(ctx context.Context, input gcpcl.CreateI
return c.createInstancesErr
}
func (c *stubGcpClient) CreateServiceAccount(ctx context.Context, input gcpcl.ServiceAccountInput) (string, error) {
return gcpshared.ServiceAccountKey{}.ToCloudServiceAccountURI(), c.createServiceAccountErr
}
func (c *stubGcpClient) CreateLoadBalancers(ctx context.Context) error {
return c.createLoadBalancerErr
}
@ -445,11 +413,6 @@ func (c *stubGcpClient) TerminateInstances(context.Context) error {
return c.terminateInstancesErr
}
func (c *stubGcpClient) TerminateServiceAccount(context.Context) error {
c.terminateServiceAccountCalled = true
return c.terminateServiceAccountErr
}
func (c *stubGcpClient) TerminateLoadBalancers(context.Context) error {
return c.terminateLoadBalancerErr
}

View File

@ -34,18 +34,7 @@ func (c *ServiceAccountCreator) Create(ctx context.Context, stat state.Constella
provider := cloudprovider.FromString(stat.CloudProvider)
switch provider {
case cloudprovider.GCP:
cl, err := c.newGCPClient(ctx)
if err != nil {
return "", state.ConstellationState{}, err
}
defer cl.Close()
serviceAccount, stat, err := c.createServiceAccountGCP(ctx, cl, stat, config)
if err != nil {
return "", state.ConstellationState{}, err
}
return serviceAccount, stat, err
return "", state.ConstellationState{}, fmt.Errorf("creating service account not supported for GCP")
case cloudprovider.Azure:
cl, err := c.newAzureClient(stat.AzureSubscription, stat.AzureTenant)
if err != nil {
@ -65,22 +54,6 @@ func (c *ServiceAccountCreator) Create(ctx context.Context, stat state.Constella
}
}
func (c *ServiceAccountCreator) createServiceAccountGCP(ctx context.Context, cl gcpclient,
stat state.ConstellationState, config *config.Config,
) (string, state.ConstellationState, error) {
cl.SetState(stat)
input := gcpcl.ServiceAccountInput{
Roles: config.Provider.GCP.ServiceAccountRoles,
}
serviceAccount, err := cl.CreateServiceAccount(ctx, input)
if err != nil {
return "", state.ConstellationState{}, fmt.Errorf("creating service account: %w", err)
}
return serviceAccount, cl.GetState(), nil
}
func (c *ServiceAccountCreator) createServiceAccountAzure(ctx context.Context, cl azureclient,
stat state.ConstellationState, _ *config.Config,
) (string, state.ConstellationState, error) {

View File

@ -6,27 +6,12 @@ import (
"testing"
"github.com/edgelesssys/constellation/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/internal/cloud/cloudtypes"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/state"
"github.com/stretchr/testify/assert"
)
func TestServiceAccountCreator(t *testing.T) {
someGCPState := func() state.ConstellationState {
return state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPProject: "project",
GCPWorkerInstances: cloudtypes.Instances{},
GCPControlPlaneInstances: cloudtypes.Instances{},
GCPWorkerInstanceGroup: "workers-group",
GCPControlPlaneInstanceGroup: "controlplane-group",
GCPWorkerInstanceTemplate: "template",
GCPControlPlaneInstanceTemplate: "template",
GCPNetwork: "network",
GCPFirewalls: []string{},
}
}
someAzureState := func() state.ConstellationState {
return state.ConstellationState{
CloudProvider: cloudprovider.Azure.String(),
@ -42,32 +27,6 @@ func TestServiceAccountCreator(t *testing.T) {
wantErr bool
wantStateMutator func(*state.ConstellationState)
}{
"gcp": {
newGCPClient: func(ctx context.Context) (gcpclient, error) {
return &fakeGcpClient{}, nil
},
state: someGCPState(),
config: config.Default(),
wantStateMutator: func(stat *state.ConstellationState) {
stat.GCPServiceAccount = "service-account@project.iam.gserviceaccount.com"
},
},
"gcp newGCPClient error": {
newGCPClient: func(ctx context.Context) (gcpclient, error) {
return nil, someErr
},
state: someGCPState(),
config: config.Default(),
wantErr: true,
},
"gcp client createServiceAccount error": {
newGCPClient: func(ctx context.Context) (gcpclient, error) {
return &stubGcpClient{createServiceAccountErr: someErr}, nil
},
state: someGCPState(),
config: config.Default(),
wantErr: true,
},
"azure": {
newAzureClient: func(subscriptionID, tenantID string) (azureclient, error) {
return &fakeAzureClient{}, nil

View File

@ -65,7 +65,8 @@ func (t *Terminator) terminateGCP(ctx context.Context, cl gcpclient, state state
if err := cl.TerminateVPCs(ctx); err != nil {
return err
}
return cl.TerminateServiceAccount(ctx)
return nil
}
func (t *Terminator) terminateAzure(ctx context.Context, cl azureclient, state state.ConstellationState) error {

View File

@ -29,7 +29,6 @@ func TestTerminator(t *testing.T) {
GCPControlPlaneInstanceTemplate: "template",
GCPNetwork: "network",
GCPFirewalls: []string{"a", "b", "c"},
GCPServiceAccount: "service-account@project.iam.gserviceaccount.com",
}
}
someAzureState := func() state.ConstellationState {
@ -80,11 +79,6 @@ func TestTerminator(t *testing.T) {
state: someGCPState(),
wantErr: true,
},
"gcp terminateServiceAccount error": {
gcpclient: &stubGcpClient{terminateServiceAccountErr: someErr},
state: someGCPState(),
wantErr: true,
},
"azure": {
azureclient: &stubAzureClient{},
state: someAzureState(),
@ -135,7 +129,6 @@ func TestTerminator(t *testing.T) {
assert.True(cl.terminateFirewallCalled)
assert.True(cl.terminateInstancesCalled)
assert.True(cl.terminateVPCsCalled)
assert.True(cl.terminateServiceAccountCalled)
assert.True(cl.closeCalled)
case cloudprovider.Azure:
cl := tc.azureclient.(*stubAzureClient)

View File

@ -24,6 +24,7 @@ import (
"github.com/edgelesssys/constellation/internal/crypto"
"github.com/edgelesssys/constellation/internal/deploy/ssh"
"github.com/edgelesssys/constellation/internal/file"
"github.com/edgelesssys/constellation/internal/gcpshared"
"github.com/edgelesssys/constellation/internal/grpc/dialer"
grpcRetry "github.com/edgelesssys/constellation/internal/grpc/retry"
"github.com/edgelesssys/constellation/internal/license"
@ -116,13 +117,22 @@ func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator
return err
}
cmd.Println("Creating service account ...")
serviceAccount, stat, err := serviceAccCreator.Create(cmd.Context(), stat, config)
if err != nil {
return err
}
if err := fileHandler.WriteJSON(constants.StateFilename, stat, file.OptOverwrite); err != nil {
return err
var serviceAccURI string
// Temporary legacy flow for Azure.
if provider == cloudprovider.Azure {
cmd.Println("Creating service account ...")
serviceAccURI, stat, err = serviceAccCreator.Create(cmd.Context(), stat, config)
if err != nil {
return err
}
if err := fileHandler.WriteJSON(constants.StateFilename, stat, file.OptOverwrite); err != nil {
return err
}
} else {
serviceAccURI, err = getMarschaledServiceAccountURI(provider, config, fileHandler)
if err != nil {
return err
}
}
workers, err := getScalingGroupsFromState(stat, config)
@ -150,7 +160,7 @@ func initialize(cmd *cobra.Command, newDialer func(validator *cloudcmd.Validator
StorageUri: kms.NoStoreURI,
KeyEncryptionKeyId: "",
UseExistingKek: false,
CloudServiceAccountUri: serviceAccount,
CloudServiceAccountUri: serviceAccURI,
KubernetesVersion: config.KubernetesVersion,
SshUserKeys: ssh.ToProtoSlice(sshUsers),
HelmDeployments: helmDeployments,
@ -352,6 +362,29 @@ func readIPFromIDFile(fileHandler file.Handler) (string, error) {
return idFile.IP, nil
}
func getMarschaledServiceAccountURI(provider cloudprovider.Provider, config *config.Config, fileHandler file.Handler) (string, error) {
switch provider {
case cloudprovider.GCP:
path := config.Provider.GCP.ServiceAccountKeyPath
var key gcpshared.ServiceAccountKey
if err := fileHandler.ReadJSON(path, &key); err != nil {
return "", fmt.Errorf("reading service account key from path %q: %w", path, err)
}
return key.ToCloudServiceAccountURI(), nil
case cloudprovider.Azure:
return "", fmt.Errorf("TODO")
case cloudprovider.QEMU:
return "", nil // QEMU does not use service account keys
default:
return "", fmt.Errorf("unsupported cloud provider %q", provider)
}
}
func getScalingGroupsFromState(stat state.ConstellationState, config *config.Config) (workers cloudtypes.ScalingGroup, err error) {
switch cloudprovider.FromString(stat.CloudProvider) {
case cloudprovider.GCP:

View File

@ -20,6 +20,7 @@ import (
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/internal/file"
"github.com/edgelesssys/constellation/internal/gcpshared"
"github.com/edgelesssys/constellation/internal/grpc/atlscredentials"
"github.com/edgelesssys/constellation/internal/grpc/dialer"
"github.com/edgelesssys/constellation/internal/grpc/testdialer"
@ -43,16 +44,19 @@ func TestInitArgumentValidation(t *testing.T) {
}
func TestInitialize(t *testing.T) {
testGcpState := state.ConstellationState{
testGcpState := &state.ConstellationState{
CloudProvider: "GCP",
GCPWorkerInstances: cloudtypes.Instances{"id-0": {}, "id-1": {}},
}
testAzureState := state.ConstellationState{
gcpServiceAccKey := &gcpshared.ServiceAccountKey{
Type: "service_account",
}
testAzureState := &state.ConstellationState{
CloudProvider: "Azure",
AzureWorkerInstances: cloudtypes.Instances{"id-0": {}, "id-1": {}},
AzureResourceGroup: "test",
}
testQemuState := state.ConstellationState{
testQemuState := &state.ConstellationState{
CloudProvider: "QEMU",
QEMUWorkerInstances: cloudtypes.Instances{"id-0": {}, "id-1": {}},
}
@ -61,77 +65,86 @@ func TestInitialize(t *testing.T) {
OwnerId: []byte("ownerID"),
ClusterId: []byte("clusterID"),
}
serviceAccPath := "/test/service-account.json"
someErr := errors.New("failed")
testCases := map[string]struct {
existingState state.ConstellationState
existingIDFile clusterIDsFile
serviceAccountCreator stubServiceAccountCreator
helmLoader stubHelmLoader
initServerAPI *stubInitServer
endpointFlag string
setAutoscaleFlag bool
wantErr bool
state *state.ConstellationState
existingIDFile *clusterIDsFile
serviceAccCreator serviceAccountCreator
configMutator func(*config.Config)
serviceAccKey *gcpshared.ServiceAccountKey
helmLoader stubHelmLoader
initServerAPI *stubInitServer
endpointFlag string
setAutoscaleFlag bool
wantErr bool
}{
"initialize some gcp instances": {
existingState: testGcpState,
existingIDFile: clusterIDsFile{IP: "192.0.2.1"},
initServerAPI: &stubInitServer{initResp: testInitResp},
},
"initialize some azure instances": {
existingState: testAzureState,
existingIDFile: clusterIDsFile{IP: "192.0.2.1"},
state: testGcpState,
existingIDFile: &clusterIDsFile{IP: "192.0.2.1"},
configMutator: func(c *config.Config) { c.Provider.GCP.ServiceAccountKeyPath = serviceAccPath },
serviceAccKey: gcpServiceAccKey,
initServerAPI: &stubInitServer{initResp: testInitResp},
},
"initialize some azure instances": {
state: testAzureState,
serviceAccCreator: &stubServiceAccountCreator{},
existingIDFile: &clusterIDsFile{IP: "192.0.2.1"},
initServerAPI: &stubInitServer{initResp: testInitResp},
},
"initialize some qemu instances": {
existingState: testQemuState,
existingIDFile: clusterIDsFile{IP: "192.0.2.1"},
state: testQemuState,
existingIDFile: &clusterIDsFile{IP: "192.0.2.1"},
initServerAPI: &stubInitServer{initResp: testInitResp},
},
"initialize gcp with autoscaling": {
existingState: testGcpState,
existingIDFile: clusterIDsFile{IP: "192.0.2.1"},
state: testGcpState,
existingIDFile: &clusterIDsFile{IP: "192.0.2.1"},
configMutator: func(c *config.Config) { c.Provider.GCP.ServiceAccountKeyPath = serviceAccPath },
serviceAccKey: gcpServiceAccKey,
initServerAPI: &stubInitServer{initResp: testInitResp},
setAutoscaleFlag: true,
},
"initialize azure with autoscaling": {
existingState: testAzureState,
existingIDFile: clusterIDsFile{IP: "192.0.2.1"},
initServerAPI: &stubInitServer{initResp: testInitResp},
setAutoscaleFlag: true,
state: testAzureState,
existingIDFile: &clusterIDsFile{IP: "192.0.2.1"},
serviceAccCreator: &stubServiceAccountCreator{},
initServerAPI: &stubInitServer{initResp: testInitResp},
setAutoscaleFlag: true,
},
"initialize with endpoint flag": {
existingState: testGcpState,
state: testGcpState,
configMutator: func(c *config.Config) { c.Provider.GCP.ServiceAccountKeyPath = serviceAccPath },
serviceAccKey: gcpServiceAccKey,
initServerAPI: &stubInitServer{initResp: testInitResp},
endpointFlag: "192.0.2.1",
},
"empty state": {
existingState: state.ConstellationState{},
existingIDFile: clusterIDsFile{IP: "192.0.2.1"},
state: &state.ConstellationState{},
existingIDFile: &clusterIDsFile{IP: "192.0.2.1"},
initServerAPI: &stubInitServer{},
wantErr: true,
},
"neither endpoint flag nor id file": {
existingState: state.ConstellationState{},
initServerAPI: &stubInitServer{},
wantErr: true,
state: &state.ConstellationState{},
wantErr: true,
},
"init call fails": {
existingState: testGcpState,
existingIDFile: clusterIDsFile{IP: "192.0.2.1"},
state: testGcpState,
existingIDFile: &clusterIDsFile{IP: "192.0.2.1"},
initServerAPI: &stubInitServer{initErr: someErr},
wantErr: true,
},
"fail to create service account": {
existingState: testGcpState,
existingIDFile: clusterIDsFile{IP: "192.0.2.1"},
initServerAPI: &stubInitServer{},
serviceAccountCreator: stubServiceAccountCreator{createErr: someErr},
wantErr: true,
state: testAzureState,
existingIDFile: &clusterIDsFile{IP: "192.0.2.1"},
initServerAPI: &stubInitServer{},
serviceAccCreator: &stubServiceAccountCreator{createErr: someErr},
wantErr: true,
},
"fail to load helm charts": {
existingState: testGcpState,
state: testGcpState,
helmLoader: stubHelmLoader{loadErr: someErr},
initServerAPI: &stubInitServer{initResp: testInitResp},
wantErr: true,
@ -143,6 +156,7 @@ func TestInitialize(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
// Networking
netDialer := testdialer.NewBufconnDialer()
newDialer := func(*cloudcmd.Validator) *dialer.Dialer {
return dialer.New(nil, nil, netDialer)
@ -155,30 +169,44 @@ func TestInitialize(t *testing.T) {
go initServer.Serve(listener)
defer initServer.GracefulStop()
// Command
cmd := NewInitCmd()
var out bytes.Buffer
cmd.SetOut(&out)
var errOut bytes.Buffer
cmd.SetErr(&errOut)
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
config := defaultConfigWithExpectedMeasurements(t, cloudprovider.FromString(tc.existingState.CloudProvider))
require.NoError(fileHandler.WriteYAML(constants.ConfigFilename, config))
require.NoError(fileHandler.WriteJSON(constants.StateFilename, tc.existingState, file.OptNone))
require.NoError(fileHandler.WriteJSON(constants.ClusterIDsFileName, tc.existingIDFile, file.OptNone))
// Flags
cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually
require.NoError(cmd.Flags().Set("autoscale", strconv.FormatBool(tc.setAutoscaleFlag)))
if tc.endpointFlag != "" {
require.NoError(cmd.Flags().Set("endpoint", tc.endpointFlag))
}
// File system preparation
fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
config := defaultConfigWithExpectedMeasurements(t, config.Default(), cloudprovider.FromString(tc.state.CloudProvider))
if tc.configMutator != nil {
tc.configMutator(config)
}
require.NoError(fileHandler.WriteYAML(constants.ConfigFilename, config, file.OptNone))
if tc.state != nil {
require.NoError(fileHandler.WriteJSON(constants.StateFilename, tc.state, file.OptNone))
}
if tc.existingIDFile != nil {
require.NoError(fileHandler.WriteJSON(constants.ClusterIDsFileName, tc.existingIDFile, file.OptNone))
}
if tc.serviceAccKey != nil {
require.NoError(fileHandler.WriteJSON(serviceAccPath, tc.serviceAccKey, file.OptNone))
}
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
defer cancel()
cmd.SetContext(ctx)
err := initialize(cmd, newDialer, &tc.serviceAccountCreator, fileHandler, &tc.helmLoader, &stubLicenseClient{})
err := initialize(cmd, newDialer, tc.serviceAccCreator, fileHandler, &tc.helmLoader, &stubLicenseClient{})
if tc.wantErr {
assert.Error(err)
@ -510,28 +538,30 @@ func (s *stubInitServer) Init(ctx context.Context, req *initproto.InitRequest) (
return s.initResp, s.initErr
}
func defaultConfigWithExpectedMeasurements(t *testing.T, csp cloudprovider.Provider) *config.Config {
func defaultConfigWithExpectedMeasurements(t *testing.T, conf *config.Config, csp cloudprovider.Provider) *config.Config {
t.Helper()
config := config.Default()
config.Provider.Azure.SubscriptionID = "01234567-0123-0123-0123-0123456789ab"
config.Provider.Azure.TenantID = "01234567-0123-0123-0123-0123456789ab"
config.Provider.Azure.Location = "test-location"
config.Provider.Azure.UserAssignedIdentity = "test-identity"
config.Provider.Azure.Measurements[8] = []byte("00000000000000000000000000000000")
config.Provider.Azure.Measurements[9] = []byte("11111111111111111111111111111111")
switch csp {
case cloudprovider.Azure:
conf.Provider.Azure.SubscriptionID = "01234567-0123-0123-0123-0123456789ab"
conf.Provider.Azure.TenantID = "01234567-0123-0123-0123-0123456789ab"
conf.Provider.Azure.Location = "test-location"
conf.Provider.Azure.UserAssignedIdentity = "test-identity"
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.Zone = "test-zone"
conf.Provider.GCP.Measurements[8] = []byte("00000000000000000000000000000000")
conf.Provider.GCP.Measurements[9] = []byte("11111111111111111111111111111111")
case cloudprovider.QEMU:
conf.Provider.QEMU.Measurements[8] = []byte("00000000000000000000000000000000")
conf.Provider.QEMU.Measurements[9] = []byte("11111111111111111111111111111111")
}
config.Provider.GCP.Region = "test-region"
config.Provider.GCP.Project = "test-project"
config.Provider.GCP.Zone = "test-zone"
config.Provider.GCP.Measurements[8] = []byte("00000000000000000000000000000000")
config.Provider.GCP.Measurements[9] = []byte("11111111111111111111111111111111")
config.Provider.QEMU.Measurements[8] = []byte("00000000000000000000000000000000")
config.Provider.QEMU.Measurements[9] = []byte("11111111111111111111111111111111")
config.RemoveProviderExcept(csp)
return config
conf.RemoveProviderExcept(csp)
return conf
}
type stubLicenseClient struct{}

View File

@ -7,6 +7,7 @@ import (
"testing"
"github.com/edgelesssys/constellation/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/internal/crypto/testvector"
"github.com/edgelesssys/constellation/internal/file"
@ -146,7 +147,7 @@ func TestRecover(t *testing.T) {
fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
config := defaultConfigWithExpectedMeasurements(t, cloudprovider.FromString(tc.existingState.CloudProvider))
config := defaultConfigWithExpectedMeasurements(t, config.Default(), cloudprovider.FromString(tc.existingState.CloudProvider))
require.NoError(fileHandler.WriteYAML(constants.ConfigFilename, config))
require.NoError(fileHandler.WriteJSON("constellation-mastersecret.json", masterSecret{Key: tc.masterSecret.Secret, Salt: tc.masterSecret.Salt}, file.OptNone))

View File

@ -12,6 +12,7 @@ import (
"github.com/edgelesssys/constellation/internal/atls"
"github.com/edgelesssys/constellation/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/internal/file"
"github.com/edgelesssys/constellation/internal/grpc/dialer"
@ -190,7 +191,7 @@ func TestVerify(t *testing.T) {
}
fileHandler := file.NewHandler(tc.setupFs(require))
config := defaultConfigWithExpectedMeasurements(t, tc.provider)
config := defaultConfigWithExpectedMeasurements(t, config.Default(), tc.provider)
require.NoError(fileHandler.WriteYAML(constants.ConfigFilename, config))
if tc.idFile != nil {
require.NoError(fileHandler.WriteJSON(constants.ClusterIDsFileName, tc.idFile, file.OptNone))

View File

@ -2,15 +2,12 @@ package client
import (
"context"
"time"
"github.com/googleapis/gax-go/v2"
"google.golang.org/api/iterator"
computepb "google.golang.org/genproto/googleapis/cloud/compute/v1"
adminpb "google.golang.org/genproto/googleapis/iam/admin/v1"
iampb "google.golang.org/genproto/googleapis/iam/v1"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)
type stubOperation struct {
@ -436,54 +433,6 @@ func (a stubInstanceGroupManagersAPI) ListManagedInstances(ctx context.Context,
return a.listIterator
}
type stubIAMAPI struct {
serviceAccountKeyData []byte
createErr error
createKeyErr error
deleteServiceAccountErr error
}
func (a stubIAMAPI) Close() error {
return nil
}
func (a stubIAMAPI) CreateServiceAccount(ctx context.Context, req *adminpb.CreateServiceAccountRequest, opts ...gax.CallOption) (*adminpb.ServiceAccount, error) {
if a.createErr != nil {
return nil, a.createErr
}
return &adminpb.ServiceAccount{
Name: "name",
ProjectId: "project-id",
UniqueId: "unique-id",
Email: "email",
DisplayName: "display-name",
Description: "description",
Oauth2ClientId: "oauth2-client-id",
Disabled: false,
}, nil
}
func (a stubIAMAPI) CreateServiceAccountKey(ctx context.Context, req *adminpb.CreateServiceAccountKeyRequest, opts ...gax.CallOption) (*adminpb.ServiceAccountKey, error) {
if a.createKeyErr != nil {
return nil, a.createKeyErr
}
return &adminpb.ServiceAccountKey{
Name: "name",
PrivateKeyType: adminpb.ServiceAccountPrivateKeyType_TYPE_GOOGLE_CREDENTIALS_FILE,
KeyAlgorithm: adminpb.ServiceAccountKeyAlgorithm_KEY_ALG_RSA_2048,
PrivateKeyData: a.serviceAccountKeyData,
PublicKeyData: []byte("public-key-data"),
ValidAfterTime: timestamppb.New(time.Time{}),
ValidBeforeTime: timestamppb.New(time.Time{}),
KeyOrigin: adminpb.ServiceAccountKeyOrigin_GOOGLE_PROVIDED,
KeyType: adminpb.ListServiceAccountKeysRequest_USER_MANAGED,
}, nil
}
func (a stubIAMAPI) DeleteServiceAccount(ctx context.Context, req *adminpb.DeleteServiceAccountRequest, opts ...gax.CallOption) error {
return a.deleteServiceAccountErr
}
type stubProjectsAPI struct {
getPolicyErr error
setPolicyErr error

View File

@ -55,7 +55,6 @@ type Client struct {
uid string
zone string
region string
serviceAccount string
// loadbalancer
loadbalancerIP string
@ -270,7 +269,6 @@ func (c *Client) GetState() state.ConstellationState {
GCPFirewalls: c.firewalls,
GCPNetwork: c.network,
GCPSubnetwork: c.subnetwork,
GCPServiceAccount: c.serviceAccount,
GCPLoadbalancerIPname: c.loadbalancerIPname,
}
for _, lb := range c.loadbalancers {
@ -297,7 +295,6 @@ func (c *Client) SetState(stat state.ConstellationState) {
c.controlPlaneTemplate = stat.GCPControlPlaneInstanceTemplate
c.loadbalancerIPname = stat.GCPLoadbalancerIPname
c.loadbalancerIP = stat.LoadBalancerIP
c.serviceAccount = stat.GCPServiceAccount
for _, lbName := range stat.GCPLoadbalancers {
lb := &loadBalancer{
name: lbName,

View File

@ -69,7 +69,6 @@ func TestSetGetState(t *testing.T) {
assert.Equal(state.GCPFirewalls, client.firewalls)
assert.Equal(state.GCPControlPlaneInstanceTemplate, client.controlPlaneTemplate)
assert.Equal(state.GCPWorkerInstanceTemplate, client.workerTemplate)
assert.Equal(state.GCPServiceAccount, client.serviceAccount)
assert.Equal(state.LoadBalancerIP, client.loadbalancerIP)
for _, lb := range client.loadbalancers {
assert.Contains(state.GCPLoadbalancers, lb.name)
@ -97,7 +96,6 @@ func TestSetGetState(t *testing.T) {
firewalls: state.GCPFirewalls,
workerTemplate: state.GCPWorkerInstanceTemplate,
controlPlaneTemplate: state.GCPControlPlaneInstanceTemplate,
serviceAccount: state.GCPServiceAccount,
loadbalancerIP: state.LoadBalancerIP,
loadbalancerIPname: state.GCPLoadbalancerIPname,
}
@ -111,18 +109,6 @@ func TestSetGetState(t *testing.T) {
})
}
func TestInit(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
client := Client{}
require.NoError(client.init("project", "zone", "region", "name"))
assert.Equal("project", client.project)
assert.Equal("zone", client.zone)
assert.Equal("region", client.region)
assert.Equal("name", client.name)
}
func TestBuildResourceName(t *testing.T) {
testCases := map[string]struct {
clientUID string
@ -209,6 +195,18 @@ func TestResourceURI(t *testing.T) {
}
}
func TestInit(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
client := Client{}
require.NoError(client.init("project", "zone", "region", "name"))
assert.Equal("project", client.project)
assert.Equal("zone", client.zone)
assert.Equal("region", client.region)
assert.Equal("name", client.name)
}
func TestCloseAll(t *testing.T) {
assert := assert.New(t)

View File

@ -1,120 +0,0 @@
package client
import (
"context"
"encoding/json"
"fmt"
"github.com/edgelesssys/constellation/internal/gcpshared"
adminpb "google.golang.org/genproto/googleapis/iam/admin/v1"
)
// CreateServiceAccount creates a new GCP service account and returns an account key as service account URI.
func (c *Client) CreateServiceAccount(ctx context.Context, input ServiceAccountInput) (string, error) {
insertInput := insertServiceAccountInput{
Project: c.project,
AccountID: "constellation-app-" + c.uid,
DisplayName: "constellation-app-" + c.uid,
Description: "This service account belongs to a Constellation cluster.",
}
email, err := c.insertServiceAccount(ctx, insertInput)
if err != nil {
return "", err
}
c.serviceAccount = email
iamInput := input.addIAMPolicyBindingInput(c.serviceAccount)
if err := c.addIAMPolicyBindings(ctx, iamInput); err != nil {
return "", err
}
key, err := c.createServiceAccountKey(ctx, email)
if err != nil {
return "", err
}
return key.ToCloudServiceAccountURI(), nil
}
func (c *Client) TerminateServiceAccount(ctx context.Context) error {
if c.serviceAccount == "" {
return nil
}
req := &adminpb.DeleteServiceAccountRequest{
Name: "projects/-/serviceAccounts/" + c.serviceAccount,
}
if err := c.iamAPI.DeleteServiceAccount(ctx, req); err != nil && !isNotFoundError(err) {
return fmt.Errorf("deleting service account: %w", err)
}
c.serviceAccount = ""
return nil
}
type ServiceAccountInput struct {
Roles []string
}
func (i ServiceAccountInput) addIAMPolicyBindingInput(serviceAccount string) AddIAMPolicyBindingInput {
iamPolicyBindingInput := AddIAMPolicyBindingInput{
Bindings: make([]PolicyBinding, len(i.Roles)),
}
for i, role := range i.Roles {
iamPolicyBindingInput.Bindings[i] = PolicyBinding{
ServiceAccount: serviceAccount,
Role: role,
}
}
return iamPolicyBindingInput
}
func (c *Client) insertServiceAccount(ctx context.Context, input insertServiceAccountInput) (string, error) {
req := input.createServiceAccountRequest()
account, err := c.iamAPI.CreateServiceAccount(ctx, req)
if err != nil {
return "", err
}
return account.Email, nil
}
func (c *Client) createServiceAccountKey(ctx context.Context, email string) (gcpshared.ServiceAccountKey, error) {
req := createServiceAccountKeyRequest(email)
key, err := c.iamAPI.CreateServiceAccountKey(ctx, req)
if err != nil {
return gcpshared.ServiceAccountKey{}, fmt.Errorf("creating service account key: %w", err)
}
var serviceAccountKey gcpshared.ServiceAccountKey
if err := json.Unmarshal(key.PrivateKeyData, &serviceAccountKey); err != nil {
return gcpshared.ServiceAccountKey{}, fmt.Errorf("decoding service account key JSON: %w", err)
}
return serviceAccountKey, nil
}
// insertServiceAccountInput is the input for a createServiceAccount operation.
type insertServiceAccountInput struct {
Project string
AccountID string
DisplayName string
Description string
}
func (c insertServiceAccountInput) createServiceAccountRequest() *adminpb.CreateServiceAccountRequest {
return &adminpb.CreateServiceAccountRequest{
Name: "projects/" + c.Project,
AccountId: c.AccountID,
ServiceAccount: &adminpb.ServiceAccount{
DisplayName: c.DisplayName,
Description: c.Description,
},
}
}
func createServiceAccountKeyRequest(email string) *adminpb.CreateServiceAccountKeyRequest {
return &adminpb.CreateServiceAccountKeyRequest{
Name: "projects/-/serviceAccounts/" + email,
}
}

View File

@ -1,139 +0,0 @@
package client
import (
"context"
"encoding/json"
"errors"
"testing"
"github.com/edgelesssys/constellation/internal/gcpshared"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreateServiceAccount(t *testing.T) {
require := require.New(t)
someErr := errors.New("someErr")
key := gcpshared.ServiceAccountKey{
Type: "type",
ProjectID: "project-id",
PrivateKeyID: "private-key-id",
PrivateKey: "private-key",
ClientEmail: "client-email",
ClientID: "client-id",
AuthURI: "auth-uri",
TokenURI: "token-uri",
AuthProviderX509CertURL: "auth-provider-x509-cert-url",
ClientX509CertURL: "client-x509-cert-url",
}
keyData, err := json.Marshal(key)
require.NoError(err)
testCases := map[string]struct {
iamAPI iamAPI
projectsAPI stubProjectsAPI
input ServiceAccountInput
wantErr bool
}{
"successful create": {
iamAPI: stubIAMAPI{serviceAccountKeyData: keyData},
input: ServiceAccountInput{
Roles: []string{"someRole"},
},
},
"successful create with roles": {
iamAPI: stubIAMAPI{serviceAccountKeyData: keyData},
},
"creating account fails": {
iamAPI: stubIAMAPI{createErr: someErr},
wantErr: true,
},
"creating account key fails": {
iamAPI: stubIAMAPI{createKeyErr: someErr},
wantErr: true,
},
"key data missing": {
iamAPI: stubIAMAPI{},
wantErr: true,
},
"key data corrupt": {
iamAPI: stubIAMAPI{serviceAccountKeyData: []byte("invalid key data")},
wantErr: true,
},
"retrieving iam policy bindings fails": {
iamAPI: stubIAMAPI{},
projectsAPI: stubProjectsAPI{getPolicyErr: someErr},
wantErr: true,
},
"setting iam policy bindings fails": {
iamAPI: stubIAMAPI{},
projectsAPI: stubProjectsAPI{setPolicyErr: someErr},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
client := Client{
project: "project",
zone: "zone",
name: "name",
uid: "uid",
iamAPI: tc.iamAPI,
projectsAPI: tc.projectsAPI,
}
serviceAccountKey, err := client.CreateServiceAccount(ctx, tc.input)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(key.ToCloudServiceAccountURI(), serviceAccountKey)
assert.Equal("email", client.serviceAccount)
}
})
}
}
func TestTerminateServiceAccount(t *testing.T) {
testCases := map[string]struct {
iamAPI iamAPI
wantErr bool
}{
"delete works": {
iamAPI: stubIAMAPI{},
},
"delete fails": {
iamAPI: stubIAMAPI{
deleteServiceAccountErr: errors.New("someErr"),
},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
ctx := context.Background()
client := Client{
project: "project",
zone: "zone",
name: "name",
uid: "uid",
serviceAccount: "service-account",
iamAPI: tc.iamAPI,
}
err := client.TerminateServiceAccount(ctx)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}

View File

@ -162,8 +162,8 @@ type GCPConfig struct {
// Type of a node's state disk. The type influences boot time and I/O performance. See: https://cloud.google.com/compute/docs/disks#disk-types
StateDiskType string `yaml:"stateDiskType" validate:"oneof=pd-standard pd-balanced pd-ssd"`
// description: |
// Roles added to service account.
ServiceAccountRoles []string `yaml:"serviceAccountRoles"`
// Path of service account key file. For needed service account roles, see https://constellation-docs.edgeless.systems/constellation/latest/#/getting-started/install?id=authorization
ServiceAccountKeyPath string `yaml:"serviceAccountKeyPath"`
// description: |
// Expected confidential VM measurements.
Measurements Measurements `yaml:"measurements"`
@ -232,20 +232,14 @@ func Default() *Config {
EnforcedMeasurements: []uint32{8, 9, 11, 12},
},
GCP: &GCPConfig{
Project: "",
Region: "",
Zone: "",
Image: "projects/constellation-images/global/images/constellation-v1-5-0",
ServiceAccountRoles: []string{
"roles/compute.instanceAdmin.v1",
"roles/compute.networkAdmin",
"roles/compute.securityAdmin",
"roles/storage.admin",
"roles/iam.serviceAccountUser",
},
StateDiskType: "pd-ssd",
Measurements: copyPCRMap(gcpPCRs),
EnforcedMeasurements: []uint32{0, 8, 9, 11, 12},
Project: "",
Region: "",
Zone: "",
Image: "projects/constellation-images/global/images/constellation-v1-5-0",
StateDiskType: "pd-ssd",
ServiceAccountKeyPath: "serviceAccountKey.json",
Measurements: copyPCRMap(gcpPCRs),
EnforcedMeasurements: []uint32{0, 8, 9, 11, 12},
},
QEMU: &QEMUConfig{
Measurements: copyPCRMap(qemuPCRs),

View File

@ -245,11 +245,11 @@ func init() {
GCPConfigDoc.Fields[4].Note = ""
GCPConfigDoc.Fields[4].Description = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://cloud.google.com/compute/docs/disks#disk-types"
GCPConfigDoc.Fields[4].Comments[encoder.LineComment] = "Type of a node's state disk. The type influences boot time and I/O performance. See: https://cloud.google.com/compute/docs/disks#disk-types"
GCPConfigDoc.Fields[5].Name = "serviceAccountRoles"
GCPConfigDoc.Fields[5].Type = "[]string"
GCPConfigDoc.Fields[5].Name = "serviceAccountKeyPath"
GCPConfigDoc.Fields[5].Type = "string"
GCPConfigDoc.Fields[5].Note = ""
GCPConfigDoc.Fields[5].Description = "Roles added to service account."
GCPConfigDoc.Fields[5].Comments[encoder.LineComment] = "Roles added to service account."
GCPConfigDoc.Fields[5].Description = "Path of service account key file. For needed service account roles, see https://constellation-docs.edgeless.systems/constellation/latest/#/getting-started/install?id=authorization"
GCPConfigDoc.Fields[5].Comments[encoder.LineComment] = "Path of service account key file. For needed service account roles, see https://constellation-docs.edgeless.systems/constellation/latest/#/getting-started/install?id=authorization"
GCPConfigDoc.Fields[6].Name = "measurements"
GCPConfigDoc.Fields[6].Type = "Measurements"
GCPConfigDoc.Fields[6].Note = ""

View File

@ -25,7 +25,6 @@ type ConstellationState struct {
GCPProject string `json:"gcpproject,omitempty"`
GCPZone string `json:"gcpzone,omitempty"`
GCPRegion string `json:"gcpregion,omitempty"`
GCPServiceAccount string `json:"gcpserviceaccount,omitempty"`
AzureWorkerInstances cloudtypes.Instances `json:"azureworkers,omitempty"`
AzureControlPlaneInstances cloudtypes.Instances `json:"azurecontrolplanes,omitempty"`