Split cmd package

This commit is contained in:
katexochen 2022-04-13 13:01:38 +02:00 committed by Paul Meyer
parent 63898c42bf
commit de52bf14da
36 changed files with 1875 additions and 2302 deletions

View File

@ -0,0 +1,37 @@
package cloudcmd
import (
"context"
azurecl "github.com/edgelesssys/constellation/cli/azure/client"
gcpcl "github.com/edgelesssys/constellation/cli/gcp/client"
"github.com/edgelesssys/constellation/internal/state"
)
type gcpclient interface {
GetState() (state.ConstellationState, error)
SetState(state.ConstellationState) error
CreateVPCs(ctx context.Context, input gcpcl.VPCsInput) error
CreateFirewall(ctx context.Context, input gcpcl.FirewallInput) error
CreateInstances(ctx context.Context, input gcpcl.CreateInstancesInput) error
CreateServiceAccount(ctx context.Context, input gcpcl.ServiceAccountInput) (string, error)
TerminateFirewall(ctx context.Context) error
TerminateVPCs(context.Context) error
TerminateInstances(context.Context) error
TerminateServiceAccount(ctx context.Context) error
Close() error
}
type azureclient interface {
GetState() (state.ConstellationState, error)
SetState(state.ConstellationState) error
CreateResourceGroup(ctx context.Context) error
CreateVirtualNetwork(ctx context.Context) error
CreateSecurityGroup(ctx context.Context, input azurecl.NetworkSecurityGroupInput) error
CreateInstances(ctx context.Context, input azurecl.CreateInstancesInput) error
// TODO: deprecate as soon as scale sets are available
CreateInstancesVMs(ctx context.Context, input azurecl.CreateInstancesInput) error
CreateServicePrincipal(ctx context.Context) (string, error)
TerminateResourceGroup(ctx context.Context) error
TerminateServicePrincipal(ctx context.Context) error
}

View File

@ -0,0 +1,419 @@
package cloudcmd
import (
"context"
"errors"
"strconv"
"github.com/edgelesssys/constellation/cli/azure"
azurecl "github.com/edgelesssys/constellation/cli/azure/client"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/cli/gcp"
gcpcl "github.com/edgelesssys/constellation/cli/gcp/client"
"github.com/edgelesssys/constellation/internal/state"
)
type fakeAzureClient struct {
nodes azure.Instances
coordinators azure.Instances
resourceGroup string
name string
uid string
location string
subscriptionID string
tenantID string
subnetID string
coordinatorsScaleSet string
nodesScaleSet string
networkSecurityGroup string
adAppObjectID string
}
func (c *fakeAzureClient) GetState() (state.ConstellationState, error) {
stat := state.ConstellationState{
CloudProvider: cloudprovider.Azure.String(),
AzureNodes: c.nodes,
AzureCoordinators: c.coordinators,
Name: c.name,
UID: c.uid,
AzureResourceGroup: c.resourceGroup,
AzureLocation: c.location,
AzureSubscription: c.subscriptionID,
AzureTenant: c.tenantID,
AzureSubnet: c.subnetID,
AzureNetworkSecurityGroup: c.networkSecurityGroup,
AzureNodesScaleSet: c.nodesScaleSet,
AzureCoordinatorsScaleSet: c.coordinatorsScaleSet,
AzureADAppObjectID: c.adAppObjectID,
}
return stat, nil
}
func (c *fakeAzureClient) SetState(stat state.ConstellationState) error {
c.nodes = stat.AzureNodes
c.coordinators = stat.AzureCoordinators
c.name = stat.Name
c.uid = stat.UID
c.resourceGroup = stat.AzureResourceGroup
c.location = stat.AzureLocation
c.subscriptionID = stat.AzureSubscription
c.tenantID = stat.AzureTenant
c.subnetID = stat.AzureSubnet
c.networkSecurityGroup = stat.AzureNetworkSecurityGroup
c.nodesScaleSet = stat.AzureNodesScaleSet
c.coordinatorsScaleSet = stat.AzureCoordinatorsScaleSet
c.adAppObjectID = stat.AzureADAppObjectID
return nil
}
func (c *fakeAzureClient) CreateResourceGroup(ctx context.Context) error {
c.resourceGroup = "resource-group"
return nil
}
func (c *fakeAzureClient) CreateVirtualNetwork(ctx context.Context) error {
c.subnetID = "subnet"
return nil
}
func (c *fakeAzureClient) CreateSecurityGroup(ctx context.Context, input azurecl.NetworkSecurityGroupInput) error {
c.networkSecurityGroup = "network-security-group"
return nil
}
func (c *fakeAzureClient) CreateInstances(ctx context.Context, input azurecl.CreateInstancesInput) error {
c.coordinatorsScaleSet = "coordinators-scale-set"
c.nodesScaleSet = "nodes-scale-set"
c.nodes = make(azure.Instances)
for i := 0; i < input.CountNodes; i++ {
id := "id-" + strconv.Itoa(i)
c.nodes[id] = azure.Instance{PublicIP: "192.0.2.1", PrivateIP: "192.0.2.1"}
}
c.coordinators = make(azure.Instances)
for i := 0; i < input.CountCoordinators; i++ {
id := "id-" + strconv.Itoa(i)
c.coordinators[id] = azure.Instance{PublicIP: "192.0.2.1", PrivateIP: "192.0.2.1"}
}
return nil
}
// TODO: deprecate as soon as scale sets are available.
func (c *fakeAzureClient) CreateInstancesVMs(ctx context.Context, input azurecl.CreateInstancesInput) error {
c.nodes = make(azure.Instances)
for i := 0; i < input.CountNodes; i++ {
id := "id-" + strconv.Itoa(i)
c.nodes[id] = azure.Instance{PublicIP: "192.0.2.1", PrivateIP: "192.0.2.1"}
}
c.coordinators = make(azure.Instances)
for i := 0; i < input.CountCoordinators; i++ {
id := "id-" + strconv.Itoa(i)
c.coordinators[id] = azure.Instance{PublicIP: "192.0.2.1", PrivateIP: "192.0.2.1"}
}
return nil
}
func (c *fakeAzureClient) CreateServicePrincipal(ctx context.Context) (string, error) {
c.adAppObjectID = "00000000-0000-0000-0000-000000000001"
return azurecl.ApplicationCredentials{
ClientID: "client-id",
ClientSecret: "client-secret",
}.ConvertToCloudServiceAccountURI(), nil
}
func (c *fakeAzureClient) TerminateResourceGroup(ctx context.Context) error {
if c.resourceGroup == "" {
return nil
}
c.nodes = nil
c.coordinators = nil
c.resourceGroup = ""
c.subnetID = ""
c.networkSecurityGroup = ""
c.nodesScaleSet = ""
c.coordinatorsScaleSet = ""
return nil
}
func (c *fakeAzureClient) TerminateServicePrincipal(ctx context.Context) error {
if c.adAppObjectID == "" {
return nil
}
c.adAppObjectID = ""
return nil
}
type stubAzureClient struct {
terminateResourceGroupCalled bool
terminateServicePrincipalCalled bool
getStateErr error
setStateErr error
createResourceGroupErr error
createVirtualNetworkErr error
createSecurityGroupErr error
createInstancesErr error
createServicePrincipalErr error
terminateResourceGroupErr error
terminateServicePrincipalErr error
}
func (c *stubAzureClient) GetState() (state.ConstellationState, error) {
return state.ConstellationState{}, c.getStateErr
}
func (c *stubAzureClient) SetState(state.ConstellationState) error {
return c.setStateErr
}
func (c *stubAzureClient) CreateResourceGroup(ctx context.Context) error {
return c.createResourceGroupErr
}
func (c *stubAzureClient) CreateVirtualNetwork(ctx context.Context) error {
return c.createVirtualNetworkErr
}
func (c *stubAzureClient) CreateSecurityGroup(ctx context.Context, input azurecl.NetworkSecurityGroupInput) error {
return c.createSecurityGroupErr
}
func (c *stubAzureClient) CreateInstances(ctx context.Context, input azurecl.CreateInstancesInput) error {
return c.createInstancesErr
}
// TODO: deprecate as soon as scale sets are available.
func (c *stubAzureClient) CreateInstancesVMs(ctx context.Context, input azurecl.CreateInstancesInput) error {
return c.createInstancesErr
}
func (c *stubAzureClient) CreateServicePrincipal(ctx context.Context) (string, error) {
return azurecl.ApplicationCredentials{
ClientID: "00000000-0000-0000-0000-000000000000",
ClientSecret: "secret",
}.ConvertToCloudServiceAccountURI(), c.createServicePrincipalErr
}
func (c *stubAzureClient) TerminateResourceGroup(ctx context.Context) error {
c.terminateResourceGroupCalled = true
return c.terminateResourceGroupErr
}
func (c *stubAzureClient) TerminateServicePrincipal(ctx context.Context) error {
c.terminateServicePrincipalCalled = true
return c.terminateServicePrincipalErr
}
type fakeGcpClient struct {
nodes gcp.Instances
coordinators gcp.Instances
nodesInstanceGroup string
coordinatorInstanceGroup string
coordinatorTemplate string
nodeTemplate string
network string
subnetwork string
firewalls []string
project string
uid string
name string
zone string
serviceAccount string
}
func (c *fakeGcpClient) GetState() (state.ConstellationState, error) {
stat := state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: c.nodes,
GCPCoordinators: c.coordinators,
GCPNodeInstanceGroup: c.nodesInstanceGroup,
GCPCoordinatorInstanceGroup: c.coordinatorInstanceGroup,
GCPNodeInstanceTemplate: c.nodeTemplate,
GCPCoordinatorInstanceTemplate: c.coordinatorTemplate,
GCPNetwork: c.network,
GCPSubnetwork: c.subnetwork,
GCPFirewalls: c.firewalls,
GCPProject: c.project,
Name: c.name,
UID: c.uid,
GCPZone: c.zone,
GCPServiceAccount: c.serviceAccount,
}
return stat, nil
}
func (c *fakeGcpClient) SetState(stat state.ConstellationState) error {
c.nodes = stat.GCPNodes
c.coordinators = stat.GCPCoordinators
c.nodesInstanceGroup = stat.GCPNodeInstanceGroup
c.coordinatorInstanceGroup = stat.GCPCoordinatorInstanceGroup
c.nodeTemplate = stat.GCPNodeInstanceTemplate
c.coordinatorTemplate = stat.GCPCoordinatorInstanceTemplate
c.network = stat.GCPNetwork
c.subnetwork = stat.GCPSubnetwork
c.firewalls = stat.GCPFirewalls
c.project = stat.GCPProject
c.name = stat.Name
c.uid = stat.UID
c.zone = stat.GCPZone
c.serviceAccount = stat.GCPServiceAccount
return nil
}
func (c *fakeGcpClient) CreateVPCs(ctx context.Context, input gcpcl.VPCsInput) error {
c.network = "network"
c.subnetwork = "subnetwork"
return nil
}
func (c *fakeGcpClient) CreateFirewall(ctx context.Context, input gcpcl.FirewallInput) error {
if c.network == "" {
return errors.New("client has not network")
}
var firewalls []string
for _, rule := range input.Ingress {
firewalls = append(firewalls, rule.Name)
}
c.firewalls = firewalls
return nil
}
func (c *fakeGcpClient) CreateInstances(ctx context.Context, input gcpcl.CreateInstancesInput) error {
c.coordinatorInstanceGroup = "coordinator-group"
c.nodesInstanceGroup = "nodes-group"
c.nodeTemplate = "node-template"
c.coordinatorTemplate = "coordinator-template"
c.nodes = make(gcp.Instances)
for i := 0; i < input.CountNodes; i++ {
id := "id-" + strconv.Itoa(i)
c.nodes[id] = gcp.Instance{PublicIP: "192.0.2.1", PrivateIP: "192.0.2.1"}
}
c.coordinators = make(gcp.Instances)
for i := 0; i < input.CountCoordinators; i++ {
id := "id-" + strconv.Itoa(i)
c.coordinators[id] = gcp.Instance{PublicIP: "192.0.2.1", PrivateIP: "192.0.2.1"}
}
return nil
}
func (c *fakeGcpClient) CreateServiceAccount(ctx context.Context, input gcpcl.ServiceAccountInput) (string, error) {
c.serviceAccount = "service-account@" + c.project + ".iam.gserviceaccount.com"
return gcpcl.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",
}.ConvertToCloudServiceAccountURI(), nil
}
func (c *fakeGcpClient) TerminateFirewall(ctx context.Context) error {
if len(c.firewalls) == 0 {
return nil
}
c.firewalls = nil
return nil
}
func (c *fakeGcpClient) TerminateVPCs(context.Context) error {
if len(c.firewalls) != 0 {
return errors.New("client has firewalls, which must be deleted first")
}
c.network = ""
c.subnetwork = ""
return nil
}
func (c *fakeGcpClient) TerminateInstances(context.Context) error {
c.nodeTemplate = ""
c.coordinatorTemplate = ""
c.nodesInstanceGroup = ""
c.coordinatorInstanceGroup = ""
c.nodes = nil
c.coordinators = nil
return nil
}
func (c *fakeGcpClient) TerminateServiceAccount(context.Context) error {
c.serviceAccount = ""
return nil
}
func (c *fakeGcpClient) Close() error {
return nil
}
type stubGcpClient struct {
terminateFirewallCalled bool
terminateInstancesCalled bool
terminateVPCsCalled bool
terminateServiceAccountCalled bool
closeCalled bool
getStateErr error
setStateErr error
createVPCsErr error
createFirewallErr error
createInstancesErr error
createServiceAccountErr error
terminateFirewallErr error
terminateVPCsErr error
terminateInstancesErr error
terminateServiceAccountErr error
closeErr error
}
func (c *stubGcpClient) GetState() (state.ConstellationState, error) {
return state.ConstellationState{}, c.getStateErr
}
func (c *stubGcpClient) SetState(state.ConstellationState) error {
return c.setStateErr
}
func (c *stubGcpClient) CreateVPCs(ctx context.Context, input gcpcl.VPCsInput) error {
return c.createVPCsErr
}
func (c *stubGcpClient) CreateFirewall(ctx context.Context, input gcpcl.FirewallInput) error {
return c.createFirewallErr
}
func (c *stubGcpClient) CreateInstances(ctx context.Context, input gcpcl.CreateInstancesInput) error {
return c.createInstancesErr
}
func (c *stubGcpClient) CreateServiceAccount(ctx context.Context, input gcpcl.ServiceAccountInput) (string, error) {
return gcpcl.ServiceAccountKey{}.ConvertToCloudServiceAccountURI(), c.createServiceAccountErr
}
func (c *stubGcpClient) TerminateFirewall(ctx context.Context) error {
c.terminateFirewallCalled = true
return c.terminateFirewallErr
}
func (c *stubGcpClient) TerminateVPCs(context.Context) error {
c.terminateVPCsCalled = true
return c.terminateVPCsErr
}
func (c *stubGcpClient) TerminateInstances(context.Context) error {
c.terminateInstancesCalled = true
return c.terminateInstancesErr
}
func (c *stubGcpClient) TerminateServiceAccount(context.Context) error {
c.terminateServiceAccountCalled = true
return c.terminateServiceAccountErr
}
func (c *stubGcpClient) Close() error {
c.closeCalled = true
return c.closeErr
}

View File

@ -0,0 +1,123 @@
package cloudcmd
import (
"context"
"fmt"
"io"
azurecl "github.com/edgelesssys/constellation/cli/azure/client"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/cli/gcp"
"github.com/edgelesssys/constellation/cli/gcp/client"
gcpcl "github.com/edgelesssys/constellation/cli/gcp/client"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/state"
)
// Creator creates cloud resources.
type Creator struct {
out io.Writer
newGCPClient func(ctx context.Context, project, zone, region, name string) (gcpclient, error)
newAzureClient func(subscriptionID, tenantID, name, location string) (azureclient, error)
}
// NewCreator creates a new creator.
func NewCreator(out io.Writer) *Creator {
return &Creator{
out: out,
newGCPClient: func(ctx context.Context, project, zone, region, name string) (gcpclient, error) {
return gcpcl.NewInitialized(ctx, project, zone, region, name)
},
newAzureClient: func(subscriptionID, tenantID, name, location string) (azureclient, error) {
return azurecl.NewInitialized(subscriptionID, tenantID, name, location)
},
}
}
// Create creates the handed amount of instances and all the needed resources.
func (c *Creator) Create(ctx context.Context, provider cloudprovider.CloudProvider, config *config.Config, name, insType string, coordCount, nodeCount int,
) (state.ConstellationState, error) {
switch provider {
case cloudprovider.GCP:
cl, err := c.newGCPClient(
ctx,
*config.Provider.GCP.Project,
*config.Provider.GCP.Zone,
*config.Provider.GCP.Region,
name,
)
if err != nil {
return state.ConstellationState{}, err
}
defer cl.Close()
return c.createGCP(ctx, cl, config, insType, coordCount, nodeCount)
case cloudprovider.Azure:
cl, err := c.newAzureClient(
*config.Provider.Azure.SubscriptionID,
*config.Provider.Azure.TenantID,
name,
*config.Provider.Azure.Location,
)
if err != nil {
return state.ConstellationState{}, err
}
return c.createAzure(ctx, cl, config, insType, coordCount, nodeCount)
default:
return state.ConstellationState{}, fmt.Errorf("unsupported cloud provider: %s", provider)
}
}
func (c *Creator) createGCP(ctx context.Context, cl gcpclient, config *config.Config, insType string, coordCount, nodeCount int,
) (stat state.ConstellationState, retErr error) {
defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerGCP{client: cl})
if err := cl.CreateVPCs(ctx, *config.Provider.GCP.VPCsInput); err != nil {
return state.ConstellationState{}, err
}
if err := cl.CreateFirewall(ctx, *config.Provider.GCP.FirewallInput); err != nil {
return state.ConstellationState{}, err
}
createInput := client.CreateInstancesInput{
CountCoordinators: coordCount,
CountNodes: nodeCount,
ImageId: *config.Provider.GCP.Image,
InstanceType: insType,
StateDiskSizeGB: *config.StateDiskSizeGB,
KubeEnv: gcp.KubeEnv,
DisableCVM: *config.Provider.GCP.DisableCVM,
}
if err := cl.CreateInstances(ctx, createInput); err != nil {
return state.ConstellationState{}, err
}
return cl.GetState()
}
func (c *Creator) createAzure(ctx context.Context, cl azureclient, config *config.Config, insType string, coordCount, nodeCount int,
) (stat state.ConstellationState, retErr error) {
defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerAzure{client: cl})
if err := cl.CreateResourceGroup(ctx); err != nil {
return state.ConstellationState{}, err
}
if err := cl.CreateVirtualNetwork(ctx); err != nil {
return state.ConstellationState{}, err
}
if err := cl.CreateSecurityGroup(ctx, *config.Provider.Azure.NetworkSecurityGroupInput); err != nil {
return state.ConstellationState{}, err
}
createInput := azurecl.CreateInstancesInput{
CountCoordinators: coordCount,
CountNodes: nodeCount,
InstanceType: insType,
StateDiskSizeGB: *config.StateDiskSizeGB,
Image: *config.Provider.Azure.Image,
UserAssingedIdentity: *config.Provider.Azure.UserAssignedIdentity,
}
if err := cl.CreateInstances(ctx, createInput); err != nil {
return state.ConstellationState{}, err
}
return cl.GetState()
}

View File

@ -0,0 +1,187 @@
package cloudcmd
import (
"bytes"
"context"
"errors"
"testing"
"github.com/edgelesssys/constellation/cli/azure"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/cli/gcp"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/state"
"github.com/stretchr/testify/assert"
)
func TestCreator(t *testing.T) {
wantGCPState := state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPProject: "project",
GCPCoordinators: gcp.Instances{
"id-0": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
"id-1": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
},
GCPNodes: gcp.Instances{
"id-0": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
"id-1": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
"id-2": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
},
GCPNodeInstanceGroup: "nodes-group",
GCPCoordinatorInstanceGroup: "coordinator-group",
GCPNodeInstanceTemplate: "node-template",
GCPCoordinatorInstanceTemplate: "coordinator-template",
GCPNetwork: "network",
GCPSubnetwork: "subnetwork",
GCPFirewalls: []string{"coordinator", "wireguard", "ssh"},
}
wantAzureState := state.ConstellationState{
CloudProvider: cloudprovider.Azure.String(),
AzureCoordinators: azure.Instances{
"id-0": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
"id-1": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
},
AzureNodes: azure.Instances{
"id-0": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
"id-1": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
"id-2": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
},
AzureResourceGroup: "resource-group",
AzureSubnet: "subnet",
AzureNetworkSecurityGroup: "network-security-group",
AzureNodesScaleSet: "nodes-scale-set",
AzureCoordinatorsScaleSet: "coordinators-scale-set",
}
someErr := errors.New("failed")
testCases := map[string]struct {
gcpclient gcpclient
newGCPClientErr error
azureclient azureclient
newAzureClientErr error
provider cloudprovider.CloudProvider
config *config.Config
wantState state.ConstellationState
wantErr bool
wantRollback bool // Use only together with stubClients.
}{
"gcp": {
gcpclient: &fakeGcpClient{project: "project"},
provider: cloudprovider.GCP,
config: config.Default(),
wantState: wantGCPState,
},
"gcp newGCPClient error": {
newGCPClientErr: someErr,
provider: cloudprovider.GCP,
config: config.Default(),
wantErr: true,
},
"gcp CreateVPCs error": {
gcpclient: &stubGcpClient{createVPCsErr: someErr},
provider: cloudprovider.GCP,
config: config.Default(),
wantErr: true,
wantRollback: true,
},
"gcp CreateFirewall error": {
gcpclient: &stubGcpClient{createFirewallErr: someErr},
provider: cloudprovider.GCP,
config: config.Default(),
wantErr: true,
wantRollback: true,
},
"gcp CreateInstances error": {
gcpclient: &stubGcpClient{createInstancesErr: someErr},
provider: cloudprovider.GCP,
config: config.Default(),
wantErr: true,
wantRollback: true,
},
"azure": {
azureclient: &fakeAzureClient{},
provider: cloudprovider.Azure,
config: config.Default(),
wantState: wantAzureState,
},
"azure newAzureClient error": {
newAzureClientErr: someErr,
provider: cloudprovider.Azure,
config: config.Default(),
wantErr: true,
},
"azure CreateResourceGroup error": {
azureclient: &stubAzureClient{createResourceGroupErr: someErr},
provider: cloudprovider.Azure,
config: config.Default(),
wantErr: true,
wantRollback: true,
},
"azure CreateVirtualNetwork error": {
azureclient: &stubAzureClient{createVirtualNetworkErr: someErr},
provider: cloudprovider.Azure,
config: config.Default(),
wantErr: true,
wantRollback: true,
},
"azure CreateSecurityGroup error": {
azureclient: &stubAzureClient{createSecurityGroupErr: someErr},
provider: cloudprovider.Azure,
config: config.Default(),
wantErr: true,
wantRollback: true,
},
"azure CreateInstances error": {
azureclient: &stubAzureClient{createInstancesErr: someErr},
provider: cloudprovider.Azure,
config: config.Default(),
wantErr: true,
wantRollback: true,
},
"unknown provider": {
provider: cloudprovider.Unknown,
config: config.Default(),
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
creator := &Creator{
out: &bytes.Buffer{},
newGCPClient: func(ctx context.Context, project, zone, region, name string) (gcpclient, error) {
return tc.gcpclient, tc.newGCPClientErr
},
newAzureClient: func(subscriptionID, tenantID, name, location string) (azureclient, error) {
return tc.azureclient, tc.newAzureClientErr
},
}
state, err := creator.Create(context.Background(), tc.provider, tc.config, "name", "type", 2, 3)
if tc.wantErr {
assert.Error(err)
if tc.wantRollback {
switch tc.provider {
case cloudprovider.GCP:
cl := tc.gcpclient.(*stubGcpClient)
assert.True(cl.terminateFirewallCalled)
assert.True(cl.terminateInstancesCalled)
assert.True(cl.terminateVPCsCalled)
assert.True(cl.closeCalled)
case cloudprovider.Azure:
cl := tc.azureclient.(*stubAzureClient)
assert.True(cl.terminateResourceGroupCalled)
}
}
} else {
assert.NoError(err)
assert.Equal(tc.wantState, state)
}
})
}
}

View File

@ -1,4 +1,4 @@
package cmd package cloudcmd
import ( import (
"context" "context"
@ -47,14 +47,3 @@ type rollbackerAzure struct {
func (r *rollbackerAzure) rollback(ctx context.Context) error { func (r *rollbackerAzure) rollback(ctx context.Context) error {
return r.client.TerminateResourceGroup(ctx) return r.client.TerminateResourceGroup(ctx)
} }
type rollbackerAWS struct {
client ec2client
}
func (r *rollbackerAWS) rollback(ctx context.Context) error {
var err error
err = multierr.Append(err, r.client.TerminateInstances(ctx))
err = multierr.Append(err, r.client.DeleteSecurityGroup(ctx))
return err
}

View File

@ -0,0 +1,104 @@
package cloudcmd
import (
"context"
"fmt"
azurecl "github.com/edgelesssys/constellation/cli/azure/client"
"github.com/edgelesssys/constellation/cli/cloudprovider"
gcpcl "github.com/edgelesssys/constellation/cli/gcp/client"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/state"
)
// ServieAccountCreator creates service accounts.
type ServiceAccountCreator struct {
newGCPClient func(ctx context.Context) (gcpclient, error)
newAzureClient func(subscriptionID, tenantID string) (azureclient, error)
}
func NewServiceAccountCreator() *ServiceAccountCreator {
return &ServiceAccountCreator{
newGCPClient: func(ctx context.Context) (gcpclient, error) {
return gcpcl.NewFromDefault(ctx)
},
newAzureClient: func(subscriptionID, tenantID string) (azureclient, error) {
return azurecl.NewFromDefault(subscriptionID, tenantID)
},
}
}
// Create creates a new cloud provider service account with access to the created resources.
func (c *ServiceAccountCreator) Create(ctx context.Context, stat state.ConstellationState, config *config.Config,
) (string, state.ConstellationState, error) {
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
case cloudprovider.Azure:
cl, err := c.newAzureClient(stat.AzureSubscription, stat.AzureTenant)
if err != nil {
return "", state.ConstellationState{}, err
}
serviceAccount, stat, err := c.createServiceAccountAzure(ctx, cl, stat, config)
if err != nil {
return "", state.ConstellationState{}, err
}
return serviceAccount, stat, err
default:
return "", state.ConstellationState{}, fmt.Errorf("unsupported provider: %s", provider)
}
}
func (c *ServiceAccountCreator) createServiceAccountGCP(ctx context.Context, cl gcpclient,
stat state.ConstellationState, config *config.Config,
) (string, state.ConstellationState, error) {
if err := cl.SetState(stat); err != nil {
return "", state.ConstellationState{}, fmt.Errorf("failed to set state while creating service account: %w", err)
}
input := gcpcl.ServiceAccountInput{
Roles: *config.Provider.GCP.ServiceAccountRoles,
}
serviceAccount, err := cl.CreateServiceAccount(ctx, input)
if err != nil {
return "", state.ConstellationState{}, fmt.Errorf("failed to create service account: %w", err)
}
stat, err = cl.GetState()
if err != nil {
return "", state.ConstellationState{}, fmt.Errorf("failed to get state after creating service account: %w", err)
}
return serviceAccount, stat, nil
}
func (c *ServiceAccountCreator) createServiceAccountAzure(ctx context.Context, cl azureclient,
stat state.ConstellationState, config *config.Config,
) (string, state.ConstellationState, error) {
if err := cl.SetState(stat); err != nil {
return "", state.ConstellationState{}, fmt.Errorf("failed to set state while creating service account: %w", err)
}
serviceAccount, err := cl.CreateServicePrincipal(ctx)
if err != nil {
return "", state.ConstellationState{}, fmt.Errorf("failed to create service account: %w", err)
}
stat, err = cl.GetState()
if err != nil {
return "", state.ConstellationState{}, fmt.Errorf("failed to get state after creating service account: %w", err)
}
return serviceAccount, stat, nil
}

View File

@ -0,0 +1,157 @@
package cloudcmd
import (
"context"
"errors"
"testing"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/cli/gcp"
"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",
GCPNodes: gcp.Instances{},
GCPCoordinators: gcp.Instances{},
GCPNodeInstanceGroup: "nodes-group",
GCPCoordinatorInstanceGroup: "coord-group",
GCPNodeInstanceTemplate: "template",
GCPCoordinatorInstanceTemplate: "template",
GCPNetwork: "network",
GCPFirewalls: []string{},
}
}
someAzureState := func() state.ConstellationState {
return state.ConstellationState{
CloudProvider: cloudprovider.Azure.String(),
}
}
someErr := errors.New("failed")
testCases := map[string]struct {
newGCPClient func(ctx context.Context) (gcpclient, error)
newAzureClient func(subscriptionID, tenantID string) (azureclient, error)
state state.ConstellationState
config *config.Config
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 setState error": {
newGCPClient: func(ctx context.Context) (gcpclient, error) {
return &stubGcpClient{setStateErr: someErr}, nil
},
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,
},
"gcp client getState error": {
newGCPClient: func(ctx context.Context) (gcpclient, error) {
return &stubGcpClient{getStateErr: someErr}, nil
},
state: someGCPState(),
config: config.Default(),
wantErr: true,
},
"azure": {
newAzureClient: func(subscriptionID, tenantID string) (azureclient, error) {
return &fakeAzureClient{}, nil
},
state: someAzureState(),
config: config.Default(),
wantStateMutator: func(stat *state.ConstellationState) {
stat.AzureADAppObjectID = "00000000-0000-0000-0000-000000000001"
},
},
"azure newAzureClient error": {
newAzureClient: func(subscriptionID, tenantID string) (azureclient, error) {
return nil, someErr
},
state: someAzureState(),
config: config.Default(),
wantErr: true,
},
"azure client setState error": {
newAzureClient: func(subscriptionID, tenantID string) (azureclient, error) {
return &stubAzureClient{setStateErr: someErr}, nil
},
state: someAzureState(),
config: config.Default(),
wantErr: true,
},
"azure client createServiceAccount error": {
newAzureClient: func(subscriptionID, tenantID string) (azureclient, error) {
return &stubAzureClient{createServicePrincipalErr: someErr}, nil
},
state: someAzureState(),
config: config.Default(),
wantErr: true,
},
"azure client getState error": {
newAzureClient: func(subscriptionID, tenantID string) (azureclient, error) {
return &stubAzureClient{getStateErr: someErr}, nil
},
state: someAzureState(),
config: config.Default(),
wantErr: true,
},
"unknown cloud provider": {
state: state.ConstellationState{},
config: config.Default(),
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
creator := &ServiceAccountCreator{
newGCPClient: tc.newGCPClient,
newAzureClient: tc.newAzureClient,
}
serviceAccount, state, err := creator.Create(context.Background(), tc.state, tc.config)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.NotEmpty(serviceAccount)
tc.wantStateMutator(&tc.state)
assert.Equal(tc.state, state)
}
})
}
}

View File

@ -0,0 +1,79 @@
package cloudcmd
import (
"context"
"fmt"
azurecl "github.com/edgelesssys/constellation/cli/azure/client"
"github.com/edgelesssys/constellation/cli/cloudprovider"
gcpcl "github.com/edgelesssys/constellation/cli/gcp/client"
"github.com/edgelesssys/constellation/internal/state"
)
// Terminator deletes cloud provider resources.
type Terminator struct {
newGCPClient func(ctx context.Context) (gcpclient, error)
newAzureClient func(subscriptionID, tenantID string) (azureclient, error)
}
// NewTerminator create a new cloud terminator.
func NewTerminator() *Terminator {
return &Terminator{
newGCPClient: func(ctx context.Context) (gcpclient, error) {
return gcpcl.NewFromDefault(ctx)
},
newAzureClient: func(subscriptionID, tenantID string) (azureclient, error) {
return azurecl.NewFromDefault(subscriptionID, tenantID)
},
}
}
// Terminate deletes the could provider resources defined in the constellation state.
func (t *Terminator) Terminate(ctx context.Context, state state.ConstellationState) error {
provider := cloudprovider.FromString(state.CloudProvider)
switch provider {
case cloudprovider.GCP:
cl, err := t.newGCPClient(ctx)
if err != nil {
return err
}
defer cl.Close()
return t.terminateGCP(ctx, cl, state)
case cloudprovider.Azure:
cl, err := t.newAzureClient(state.AzureSubscription, state.AzureTenant)
if err != nil {
return err
}
return t.terminateAzure(ctx, cl, state)
default:
return fmt.Errorf("unsupported provider: %s", provider)
}
}
func (t *Terminator) terminateGCP(ctx context.Context, cl gcpclient, state state.ConstellationState) error {
if err := cl.SetState(state); err != nil {
return err
}
if err := cl.TerminateInstances(ctx); err != nil {
return err
}
if err := cl.TerminateFirewall(ctx); err != nil {
return err
}
if err := cl.TerminateVPCs(ctx); err != nil {
return err
}
return cl.TerminateServiceAccount(ctx)
}
func (t *Terminator) terminateAzure(ctx context.Context, cl azureclient, state state.ConstellationState) error {
if err := cl.SetState(state); err != nil {
return err
}
if err := cl.TerminateServicePrincipal(ctx); err != nil {
return err
}
return cl.TerminateResourceGroup(ctx)
}

View File

@ -0,0 +1,159 @@
package cloudcmd
import (
"context"
"errors"
"testing"
"github.com/edgelesssys/constellation/cli/azure"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/cli/gcp"
"github.com/edgelesssys/constellation/internal/state"
"github.com/stretchr/testify/assert"
)
func TestTerminator(t *testing.T) {
someGCPState := func() state.ConstellationState {
return state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPProject: "project",
GCPNodes: gcp.Instances{
"id-0": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
"id-1": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
},
GCPCoordinators: gcp.Instances{
"id-c": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
},
GCPNodeInstanceGroup: "nodes-group",
GCPCoordinatorInstanceGroup: "coord-group",
GCPNodeInstanceTemplate: "template",
GCPCoordinatorInstanceTemplate: "template",
GCPNetwork: "network",
GCPFirewalls: []string{"a", "b", "c"},
GCPServiceAccount: "service-account@project.iam.gserviceaccount.com",
}
}
someAzureState := func() state.ConstellationState {
return state.ConstellationState{
CloudProvider: cloudprovider.Azure.String(),
AzureNodes: azure.Instances{
"id-0": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
"id-1": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
},
AzureCoordinators: azure.Instances{
"id-c": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
},
AzureResourceGroup: "group",
AzureADAppObjectID: "00000000-0000-0000-0000-000000000001",
}
}
someErr := errors.New("failed")
testCases := map[string]struct {
gcpclient gcpclient
newGCPClientErr error
azureclient azureclient
newAzureClientErr error
state state.ConstellationState
wantErr bool
}{
"gcp": {
gcpclient: &stubGcpClient{},
state: someGCPState(),
},
"gcp newGCPClient error": {
newGCPClientErr: someErr,
state: someGCPState(),
wantErr: true,
},
"gcp setState error": {
gcpclient: &stubGcpClient{setStateErr: someErr},
state: someGCPState(),
wantErr: true,
},
"gcp terminateInstances error": {
gcpclient: &stubGcpClient{terminateInstancesErr: someErr},
state: someGCPState(),
wantErr: true,
},
"gcp terminateFirewall error": {
gcpclient: &stubGcpClient{terminateFirewallErr: someErr},
state: someGCPState(),
wantErr: true,
},
"gcp terminateVPCs error": {
gcpclient: &stubGcpClient{terminateVPCsErr: someErr},
state: someGCPState(),
wantErr: true,
},
"gcp terminateServiceAccount error": {
gcpclient: &stubGcpClient{terminateServiceAccountErr: someErr},
state: someGCPState(),
wantErr: true,
},
"azure": {
azureclient: &stubAzureClient{},
state: someAzureState(),
},
"azure newAzureClient error": {
newAzureClientErr: someErr,
state: someAzureState(),
wantErr: true,
},
"azure setState error": {
azureclient: &stubAzureClient{setStateErr: someErr},
state: someAzureState(),
wantErr: true,
},
"azure terminateServicePrincipal error": {
azureclient: &stubAzureClient{terminateServicePrincipalErr: someErr},
state: someAzureState(),
wantErr: true,
},
"azure terminateResourceGroup error": {
azureclient: &stubAzureClient{terminateResourceGroupErr: someErr},
state: someAzureState(),
wantErr: true,
},
"unknown cloud provider": {
state: state.ConstellationState{},
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
terminator := &Terminator{
newGCPClient: func(ctx context.Context) (gcpclient, error) {
return tc.gcpclient, tc.newGCPClientErr
},
newAzureClient: func(subscriptionID, tenantID string) (azureclient, error) {
return tc.azureclient, tc.newAzureClientErr
},
}
err := terminator.Terminate(context.Background(), tc.state)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
switch cloudprovider.FromString(tc.state.CloudProvider) {
case cloudprovider.GCP:
cl := tc.gcpclient.(*stubGcpClient)
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)
assert.True(cl.terminateResourceGroupCalled)
assert.True(cl.terminateServicePrincipalCalled)
}
}
})
}
}

View File

@ -1,5 +1,7 @@
package cloudprovider package cloudprovider
import "strings"
//go:generate stringer -type=CloudProvider //go:generate stringer -type=CloudProvider
// CloudProvider is cloud provider used by the CLI. // CloudProvider is cloud provider used by the CLI.
@ -11,3 +13,18 @@ const (
Azure Azure
GCP GCP
) )
// FromString returns cloud provider from string.
func FromString(s string) CloudProvider {
s = strings.ToLower(s)
switch s {
case "aws":
return AWS
case "azure":
return Azure
case "gcp":
return GCP
default:
return Unknown
}
}

View File

@ -1,22 +0,0 @@
package cmd
import (
"context"
"github.com/edgelesssys/constellation/cli/azure/client"
"github.com/edgelesssys/constellation/internal/state"
)
type azureclient interface {
GetState() (state.ConstellationState, error)
SetState(state.ConstellationState) error
CreateResourceGroup(ctx context.Context) error
CreateVirtualNetwork(ctx context.Context) error
CreateSecurityGroup(ctx context.Context, input client.NetworkSecurityGroupInput) error
CreateInstances(ctx context.Context, input client.CreateInstancesInput) error
// TODO: deprecate as soon as scale sets are available
CreateInstancesVMs(ctx context.Context, input client.CreateInstancesInput) error
CreateServicePrincipal(ctx context.Context) (string, error)
TerminateResourceGroup(ctx context.Context) error
TerminateServicePrincipal(ctx context.Context) error
}

View File

@ -1,200 +0,0 @@
package cmd
import (
"context"
"strconv"
"github.com/edgelesssys/constellation/cli/azure"
"github.com/edgelesssys/constellation/cli/azure/client"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/internal/state"
)
type fakeAzureClient struct {
nodes azure.Instances
coordinators azure.Instances
resourceGroup string
name string
uid string
location string
subscriptionID string
tenantID string
subnetID string
coordinatorsScaleSet string
nodesScaleSet string
networkSecurityGroup string
adAppObjectID string
}
func (c *fakeAzureClient) GetState() (state.ConstellationState, error) {
stat := state.ConstellationState{
CloudProvider: cloudprovider.Azure.String(),
AzureNodes: c.nodes,
AzureCoordinators: c.coordinators,
Name: c.name,
UID: c.uid,
AzureResourceGroup: c.resourceGroup,
AzureLocation: c.location,
AzureSubscription: c.subscriptionID,
AzureTenant: c.tenantID,
AzureSubnet: c.subnetID,
AzureNetworkSecurityGroup: c.networkSecurityGroup,
AzureNodesScaleSet: c.nodesScaleSet,
AzureCoordinatorsScaleSet: c.coordinatorsScaleSet,
AzureADAppObjectID: c.adAppObjectID,
}
return stat, nil
}
func (c *fakeAzureClient) SetState(stat state.ConstellationState) error {
c.nodes = stat.AzureNodes
c.coordinators = stat.AzureCoordinators
c.name = stat.Name
c.uid = stat.UID
c.resourceGroup = stat.AzureResourceGroup
c.location = stat.AzureLocation
c.subscriptionID = stat.AzureSubscription
c.tenantID = stat.AzureTenant
c.subnetID = stat.AzureSubnet
c.networkSecurityGroup = stat.AzureNetworkSecurityGroup
c.nodesScaleSet = stat.AzureNodesScaleSet
c.coordinatorsScaleSet = stat.AzureCoordinatorsScaleSet
c.adAppObjectID = stat.AzureADAppObjectID
return nil
}
func (c *fakeAzureClient) CreateResourceGroup(ctx context.Context) error {
c.resourceGroup = "resource-group"
return nil
}
func (c *fakeAzureClient) CreateVirtualNetwork(ctx context.Context) error {
c.subnetID = "subnet"
return nil
}
func (c *fakeAzureClient) CreateSecurityGroup(ctx context.Context, input client.NetworkSecurityGroupInput) error {
c.networkSecurityGroup = "network-security-group"
return nil
}
func (c *fakeAzureClient) CreateInstances(ctx context.Context, input client.CreateInstancesInput) error {
c.coordinatorsScaleSet = "coordinators-scale-set"
c.nodesScaleSet = "nodes-scale-set"
c.nodes = make(azure.Instances)
for i := 0; i < input.CountNodes; i++ {
id := strconv.Itoa(i)
c.nodes[id] = azure.Instance{PublicIP: "192.0.2.1", PrivateIP: "192.0.2.1"}
}
c.coordinators = make(azure.Instances)
for i := 0; i < input.CountCoordinators; i++ {
id := strconv.Itoa(i)
c.coordinators[id] = azure.Instance{PublicIP: "192.0.2.1", PrivateIP: "192.0.2.1"}
}
return nil
}
// TODO: deprecate as soon as scale sets are available.
func (c *fakeAzureClient) CreateInstancesVMs(ctx context.Context, input client.CreateInstancesInput) error {
c.nodes = make(azure.Instances)
for i := 0; i < input.CountNodes; i++ {
id := strconv.Itoa(i)
c.nodes[id] = azure.Instance{PublicIP: "192.0.2.1", PrivateIP: "192.0.2.1"}
}
c.coordinators = make(azure.Instances)
for i := 0; i < input.CountCoordinators; i++ {
id := strconv.Itoa(i)
c.coordinators[id] = azure.Instance{PublicIP: "192.0.2.1", PrivateIP: "192.0.2.1"}
}
return nil
}
func (c *fakeAzureClient) CreateServicePrincipal(ctx context.Context) (string, error) {
c.adAppObjectID = "00000000-0000-0000-0000-000000000001"
return client.ApplicationCredentials{
ClientID: "client-id",
ClientSecret: "client-secret",
}.ConvertToCloudServiceAccountURI(), nil
}
func (c *fakeAzureClient) TerminateResourceGroup(ctx context.Context) error {
if c.resourceGroup == "" {
return nil
}
c.nodes = nil
c.coordinators = nil
c.resourceGroup = ""
c.subnetID = ""
c.networkSecurityGroup = ""
c.nodesScaleSet = ""
c.coordinatorsScaleSet = ""
return nil
}
func (c *fakeAzureClient) TerminateServicePrincipal(ctx context.Context) error {
if c.adAppObjectID == "" {
return nil
}
c.adAppObjectID = ""
return nil
}
type stubAzureClient struct {
terminateResourceGroupCalled bool
getStateErr error
setStateErr error
createResourceGroupErr error
createVirtualNetworkErr error
createSecurityGroupErr error
createInstancesErr error
createServicePrincipalErr error
terminateResourceGroupErr error
terminateServicePrincipalErr error
}
func (c *stubAzureClient) GetState() (state.ConstellationState, error) {
return state.ConstellationState{}, c.getStateErr
}
func (c *stubAzureClient) SetState(state.ConstellationState) error {
return c.setStateErr
}
func (c *stubAzureClient) CreateResourceGroup(ctx context.Context) error {
return c.createResourceGroupErr
}
func (c *stubAzureClient) CreateVirtualNetwork(ctx context.Context) error {
return c.createVirtualNetworkErr
}
func (c *stubAzureClient) CreateSecurityGroup(ctx context.Context, input client.NetworkSecurityGroupInput) error {
return c.createSecurityGroupErr
}
func (c *stubAzureClient) CreateInstances(ctx context.Context, input client.CreateInstancesInput) error {
return c.createInstancesErr
}
// TODO: deprecate as soon as scale sets are available.
func (c *stubAzureClient) CreateInstancesVMs(ctx context.Context, input client.CreateInstancesInput) error {
return c.createInstancesErr
}
func (c *stubAzureClient) CreateServicePrincipal(ctx context.Context) (string, error) {
return client.ApplicationCredentials{
ClientID: "00000000-0000-0000-0000-000000000000",
ClientSecret: "secret",
}.ConvertToCloudServiceAccountURI(), c.createServicePrincipalErr
}
func (c *stubAzureClient) TerminateResourceGroup(ctx context.Context) error {
c.terminateResourceGroupCalled = true
return c.terminateResourceGroupErr
}
func (c *stubAzureClient) TerminateServicePrincipal(ctx context.Context) error {
return c.terminateServicePrincipalErr
}

28
cli/cmd/cloud.go Normal file
View File

@ -0,0 +1,28 @@
package cmd
import (
"context"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/state"
)
type cloudCreator interface {
Create(
ctx context.Context,
provider cloudprovider.CloudProvider,
config *config.Config,
name, insType string,
coordCount, nodeCount int,
) (state.ConstellationState, error)
}
type cloudTerminator interface {
Terminate(context.Context, state.ConstellationState) error
}
type serviceAccountCreator interface {
Create(ctx context.Context, stat state.ConstellationState, config *config.Config,
) (string, state.ConstellationState, error)
}

49
cli/cmd/cloud_test.go Normal file
View File

@ -0,0 +1,49 @@
package cmd
import (
"context"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/state"
)
type stubCloudCreator struct {
createCalled bool
state state.ConstellationState
createErr error
}
func (c *stubCloudCreator) Create(
ctx context.Context,
provider cloudprovider.CloudProvider,
config *config.Config,
name, insType string,
coordCount, nodeCount int,
) (state.ConstellationState, error) {
c.createCalled = true
return c.state, c.createErr
}
type stubCloudTerminator struct {
called bool
terminateErr error
}
func (c *stubCloudTerminator) Terminate(context.Context, state.ConstellationState) error {
c.called = true
return c.terminateErr
}
func (c *stubCloudTerminator) Called() bool {
return c.called
}
type stubServiceAccountCreator struct {
cloudServiceAccountURI string
createErr error
}
func (c *stubServiceAccountCreator) Create(ctx context.Context, stat state.ConstellationState, config *config.Config) (string, state.ConstellationState, error) {
return c.cloudServiceAccountURI, stat, c.createErr
}

View File

@ -4,27 +4,134 @@ import (
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"strconv"
"strings"
"github.com/edgelesssys/constellation/cli/azure"
"github.com/edgelesssys/constellation/cli/cloud/cloudcmd"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/cli/file" "github.com/edgelesssys/constellation/cli/file"
"github.com/edgelesssys/constellation/cli/gcp"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/constants" "github.com/edgelesssys/constellation/internal/constants"
"github.com/spf13/afero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
func newCreateCmd() *cobra.Command { func newCreateCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "create aws|gcp|azure", Use: "create {aws|gcp|azure} C_COUNT N_COUNT TYPE",
Short: "Create instances on a cloud platform for your Constellation.", Short: "Create instances on a cloud platform for your Constellation.",
Long: "Create instances on a cloud platform for your Constellation.", Long: `Create instances on a cloud platform for your Constellation.
A Constellation with C_COUNT Coordinators and N_COUNT Nodes is created.
TYPE is the instance type used for all instances.`,
Args: cobra.MatchAll(
cobra.ExactArgs(4),
isIntGreaterZeroArg(1),
isIntGreaterZeroArg(2),
isInstanceTypeForProvider(3, 0),
warnAWS(0),
),
ValidArgsFunction: createCompletion,
RunE: runCreate,
} }
cmd.PersistentFlags().String("name", "constell", "Set this flag to create the Constellation with the specified name.") cmd.Flags().String("name", "constell", "Set this flag to create the Constellation with the specified name.")
cmd.PersistentFlags().BoolP("yes", "y", false, "Set this flag to create the Constellation without further confirmation.") cmd.Flags().BoolP("yes", "y", false, "Set this flag to create the Constellation without further confirmation.")
cmd.AddCommand(newCreateAWSCmd())
cmd.AddCommand(newCreateGCPCmd())
cmd.AddCommand(newCreateAzureCmd())
return cmd return cmd
} }
func runCreate(cmd *cobra.Command, args []string) error {
provider := cloudprovider.FromString(args[0])
countCoord, _ := strconv.Atoi(args[1]) // err checked in args validation
countNode, _ := strconv.Atoi(args[2]) // err checked in args validation
insType := strings.ToLower(args[3])
fileHandler := file.NewHandler(afero.NewOsFs())
creator := cloudcmd.NewCreator(cmd.OutOrStdout())
return create(cmd, creator, fileHandler, countCoord, countNode, provider, insType)
}
func create(cmd *cobra.Command, creator cloudCreator, fileHandler file.Handler,
countCoord, countNode int, provider cloudprovider.CloudProvider, insType string,
) (retErr error) {
flags, err := parseCreateFlags(cmd)
if err != nil {
return err
}
if err := checkDirClean(fileHandler); err != nil {
return err
}
config, err := config.FromFile(fileHandler, flags.devConfigPath)
if err != nil {
return err
}
if !flags.yes {
// Ask user to confirm action.
cmd.Printf("The following Constellation will be created:\n")
cmd.Printf("%d coordinators of type %s will be created.\n", countCoord, insType)
cmd.Printf("%d nodes of type %s will be created.\n", countNode, insType)
ok, err := askToConfirm(cmd, "Do you want to create this Constellation?")
if err != nil {
return err
}
if !ok {
cmd.Println("The creation of the Constellation was aborted.")
return nil
}
}
state, err := creator.Create(cmd.Context(), provider, config, flags.name, insType, countCoord, countNode)
if err != nil {
return err
}
if err := fileHandler.WriteJSON(constants.StateFilename, state, file.OptNone); err != nil {
return err
}
cmd.Println("Your Constellation was created successfully.")
return nil
}
// parseCreateFlags parses the flags of the create command.
func parseCreateFlags(cmd *cobra.Command) (createFlags, error) {
name, err := cmd.Flags().GetString("name")
if err != nil {
return createFlags{}, err
}
if len(name) > constellationNameLength {
return createFlags{}, fmt.Errorf(
"name for constellation too long, maximum length is %d, got %d: %s",
constellationNameLength, len(name), name,
)
}
yes, err := cmd.Flags().GetBool("yes")
if err != nil {
return createFlags{}, err
}
devConfigPath, err := cmd.Flags().GetString("dev-config")
if err != nil {
return createFlags{}, err
}
return createFlags{
name: name,
devConfigPath: devConfigPath,
yes: yes,
}, nil
}
// createFlags contains the parsed flags of the create command.
type createFlags struct {
name string
devConfigPath string
yes bool
}
// checkDirClean checks if files of a previous Constellation are left in the current working dir. // checkDirClean checks if files of a previous Constellation are left in the current working dir.
func checkDirClean(fileHandler file.Handler) error { func checkDirClean(fileHandler file.Handler) error {
if _, err := fileHandler.Stat(constants.StateFilename); !errors.Is(err, fs.ErrNotExist) { if _, err := fileHandler.Stat(constants.StateFilename); !errors.Is(err, fs.ErrNotExist) {
@ -39,3 +146,35 @@ func checkDirClean(fileHandler file.Handler) error {
return nil return nil
} }
// createCompletion handles the completion of the create command. It is frequently called
// while the user types arguments of the command to suggest completion.
func createCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
switch len(args) {
case 0:
return []string{"aws", "gcp", "azure"}, cobra.ShellCompDirectiveNoFileComp
case 1:
return []string{}, cobra.ShellCompDirectiveNoFileComp
case 2:
return []string{}, cobra.ShellCompDirectiveNoFileComp
case 3:
var instanceTypeList []string
switch args[0] {
case "aws":
instanceTypeList = []string{
"4xlarge",
"8xlarge",
"12xlarge",
"16xlarge",
"24xlarge",
}
case "gcp":
instanceTypeList = gcp.InstanceTypes
case "azure":
instanceTypeList = azure.InstanceTypes
}
return instanceTypeList, cobra.ShellCompDirectiveNoFileComp
default:
return []string{}, cobra.ShellCompDirectiveError
}
}

View File

@ -1,143 +0,0 @@
package cmd
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/edgelesssys/constellation/cli/ec2"
"github.com/edgelesssys/constellation/cli/ec2/client"
"github.com/edgelesssys/constellation/cli/file"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/constants"
)
func newCreateAWSCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "aws C_COUNT N_COUNT TYPE",
Short: "Create a Constellation of C_COUNT coordinators and N_COUNT nodes of TYPE on AWS.",
Long: "Create a Constellation of C_COUNT coordinators and N_COUNT nodes of TYPE on AWS.",
Example: "aws 1 4 2xlarge",
Args: cobra.MatchAll(
cobra.ExactArgs(3),
isValidAWSCoordinatorCount(0, 1),
isIntGreaterZeroArg(1),
isEC2InstanceType(2),
),
ValidArgsFunction: createAWSCompletion,
RunE: runCreateAWS,
}
return cmd
}
// runCreateAWS runs the create command.
func runCreateAWS(cmd *cobra.Command, args []string) error {
count, _ := strconv.Atoi(args[1]) // err already checked in args validation
count++ // single coordinator
size := strings.ToLower(args[2])
name, err := cmd.Flags().GetString("name")
if err != nil {
return err
}
devConfigName, err := cmd.Flags().GetString("dev-config")
if err != nil {
return err
}
fileHandler := file.NewHandler(afero.NewOsFs())
config, err := config.FromFile(fileHandler, devConfigName)
if err != nil {
return err
}
client, err := client.NewFromDefault(cmd.Context())
if err != nil {
return err
}
return createAWS(cmd, client, fileHandler, config, size, name, count)
}
// createAWS uses the given client to create 'count' instances of 'size'.
// After the instances are running, they are tagged with the default tags.
// On success, the state of the client is saved to the state file.
func createAWS(cmd *cobra.Command, cl ec2client, fileHandler file.Handler, config *config.Config, size, name string, count int) (retErr error) {
if err := checkDirClean(fileHandler); err != nil {
return err
}
const maxLength = 255
if len(name) > maxLength {
return fmt.Errorf("name for constellation too long, maximum length is %d: %s", maxLength, name)
}
ec2Tags := append([]ec2.Tag{}, *config.Provider.EC2.Tags...)
ec2Tags = append(ec2Tags, ec2.Tag{Key: "Name", Value: name})
ok, err := cmd.Flags().GetBool("yes")
if err != nil {
return err
}
if !ok {
// Ask user to confirm action.
cmd.Printf("The following Constellation will be created:\n")
cmd.Printf("%d nodes of size %s will be created.\n", count, size)
ok, err := askToConfirm(cmd, "Do you want to create this Constellation?")
if err != nil {
return err
}
if !ok {
cmd.Println("The creation of the Constellation was aborted.")
return nil
}
}
defer rollbackOnError(context.Background(), cmd.OutOrStdout(), &retErr, &rollbackerAWS{client: cl})
if err := cl.CreateSecurityGroup(cmd.Context(), *config.Provider.EC2.SecurityGroupInput); err != nil {
return err
}
createInput := client.CreateInput{
ImageId: *config.Provider.EC2.Image,
InstanceType: size,
Count: count,
Tags: ec2Tags,
}
if err := cl.CreateInstances(cmd.Context(), createInput); err != nil {
return err
}
stat, err := cl.GetState()
if err != nil {
return err
}
if err := fileHandler.WriteJSON(constants.StateFilename, stat, file.OptNone); err != nil {
return err
}
cmd.Println("Your Constellation was created successfully.")
return nil
}
// createAWSCompletion handels the completion of CLI arguments. It is frequently called
// while the user types arguments of the command to suggest completion.
func createAWSCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
switch len(args) {
case 0:
return []string{}, cobra.ShellCompDirectiveNoFileComp
case 1:
return []string{}, cobra.ShellCompDirectiveNoFileComp
case 2:
return []string{
"4xlarge",
"8xlarge",
"12xlarge",
"16xlarge",
"24xlarge",
}, cobra.ShellCompDirectiveDefault
default:
return []string{}, cobra.ShellCompDirectiveError
}
}

View File

@ -1,213 +0,0 @@
package cmd
import (
"bytes"
"errors"
"testing"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/cli/ec2"
"github.com/edgelesssys/constellation/cli/file"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/internal/state"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreateAWSCmdArgumentValidation(t *testing.T) {
testCases := map[string]struct {
args []string
expectErr bool
}{
"valid size 4XL": {[]string{"1", "5", "4xlarge"}, false},
"valid size 8XL": {[]string{"1", "4", "8xlarge"}, false},
"valid size 12XL": {[]string{"1", "3", "12xlarge"}, false},
"valid size 16XL": {[]string{"1", "2", "16xlarge"}, false},
"valid size 24XL": {[]string{"1", "2", "24xlarge"}, false},
"valid short 12XL": {[]string{"1", "4", "12xl"}, false},
"valid short 24XL": {[]string{"1", "2", "24xl"}, false},
"valid capitalized": {[]string{"1", "3", "24XlARge"}, false},
"valid short capitalized": {[]string{"1", "4", "16XL"}, false},
"invalid to many arguments": {[]string{"1", "2", "4xl", "2xl"}, true},
"invalid to many arguments 2": {[]string{"1", "2", "4xl", "2"}, true},
"invalid first is no int": {[]string{"xl", "2", "4xl"}, true},
"invalid first is not 1": {[]string{"2", "2", "4xl"}, true},
"invalid second is no int": {[]string{"1", "xl", "4xl"}, true},
"invalid third is no size": {[]string{"2", "1", "2"}, true},
"invalid wrong order": {[]string{"4xl", "1", "2"}, true},
}
cmd := newCreateAWSCmd()
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
err := cmd.ValidateArgs(tc.args)
if tc.expectErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}
func TestCreateAWS(t *testing.T) {
testState := state.ConstellationState{
CloudProvider: cloudprovider.AWS.String(),
EC2Instances: ec2.Instances{
"id-0": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.2",
},
"id-1": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.2",
},
"id-2": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.2",
},
},
EC2SecurityGroup: "sg-test",
}
someErr := errors.New("failed")
config := config.Default()
testCases := map[string]struct {
existingState *state.ConstellationState
client ec2client
interactive bool
interactiveStdin string
stateExpected state.ConstellationState
errExpected bool
}{
"create some instances": {
client: &fakeEc2Client{},
stateExpected: testState,
errExpected: false,
},
"state already exists": {
existingState: &testState,
client: &fakeEc2Client{},
errExpected: true,
},
"create some instances interactive": {
client: &fakeEc2Client{},
interactive: true,
interactiveStdin: "y\n",
stateExpected: testState,
errExpected: false,
},
"fail CreateSecurityGroup": {
client: &stubEc2Client{createSecurityGroupErr: someErr},
errExpected: true,
},
"fail CreateInstances": {
client: &stubEc2Client{createInstancesErr: someErr},
errExpected: true,
},
"fail GetState": {
client: &stubEc2Client{getStateErr: someErr},
errExpected: true,
},
"error on rollback": {
client: &stubEc2Client{createInstancesErr: someErr, deleteSecurityGroupErr: someErr},
errExpected: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
cmd := newCreateAWSCmd()
cmd.Flags().BoolP("yes", "y", false, "")
out := bytes.NewBufferString("")
cmd.SetOut(out)
errOut := bytes.NewBufferString("")
cmd.SetErr(errOut)
in := bytes.NewBufferString(tc.interactiveStdin)
cmd.SetIn(in)
if !tc.interactive {
require.NoError(cmd.Flags().Set("yes", "true")) // disable interactivity
}
fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
if tc.existingState != nil {
require.NoError(fileHandler.WriteJSON(constants.StateFilename, *tc.existingState, file.OptNone))
}
err := createAWS(cmd, tc.client, fileHandler, config, "xlarge", "name", 3)
if tc.errExpected {
assert.Error(err)
if stubClient, ok := tc.client.(*stubEc2Client); ok {
// Should have made a rollback on error.
assert.True(stubClient.terminateInstancesCalled)
assert.True(stubClient.deleteSecurityGroupCalled)
}
} else {
assert.NoError(err)
var stat state.ConstellationState
err := fileHandler.ReadJSON(constants.StateFilename, &stat)
assert.NoError(err)
assert.Equal(tc.stateExpected, stat)
}
})
}
}
func TestCreateAWSCompletion(t *testing.T) {
testCases := map[string]struct {
args []string
toComplete string
resultExpected []string
shellCDExpected cobra.ShellCompDirective
}{
"first arg": {
args: []string{},
toComplete: "21",
resultExpected: []string{},
shellCDExpected: cobra.ShellCompDirectiveNoFileComp,
},
"second arg": {
args: []string{"23"},
toComplete: "21",
resultExpected: []string{},
shellCDExpected: cobra.ShellCompDirectiveNoFileComp,
},
"third arg": {
args: []string{"23", "24"},
toComplete: "4xl",
resultExpected: []string{
"4xlarge",
"8xlarge",
"12xlarge",
"16xlarge",
"24xlarge",
},
shellCDExpected: cobra.ShellCompDirectiveDefault,
},
"fourth arg": {
args: []string{"23", "24", "4xlarge"},
toComplete: "xl",
resultExpected: []string{},
shellCDExpected: cobra.ShellCompDirectiveError,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
cmd := &cobra.Command{}
result, shellCD := createAWSCompletion(cmd, tc.args, tc.toComplete)
assert.Equal(tc.resultExpected, result)
assert.Equal(tc.shellCDExpected, shellCD)
})
}
}

View File

@ -1,145 +0,0 @@
package cmd
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/edgelesssys/constellation/cli/azure"
"github.com/edgelesssys/constellation/cli/azure/client"
"github.com/edgelesssys/constellation/cli/file"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/constants"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
func newCreateAzureCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "azure C_COUNT N_COUNT TYPE",
Short: "Create a Constellation of C_COUNT coordinators and N_COUNT nodes of TYPE on Azure.",
Long: "Create a Constellation of C_COUNT coordinators and N_COUNT nodes of TYPE on Azure.",
Args: cobra.MatchAll(
cobra.ExactArgs(3),
isIntGreaterZeroArg(0),
isIntGreaterZeroArg(1),
isAzureInstanceType(2),
),
ValidArgsFunction: createAzureCompletion,
RunE: runCreateAzure,
}
return cmd
}
// runCreateAzure runs the create command.
func runCreateAzure(cmd *cobra.Command, args []string) error {
countCoordinators, _ := strconv.Atoi(args[0]) // err already checked in args validation
countNodes, _ := strconv.Atoi(args[1]) // err already checked in args validation
size := strings.ToLower(args[2])
subscriptionID := "0d202bbb-4fa7-4af8-8125-58c269a05435" // TODO: This will be user input
tenantID := "adb650a8-5da3-4b15-b4b0-3daf65ff7626" // TODO: This will be user input
location := "North Europe" // TODO: This will be user input
name, err := cmd.Flags().GetString("name")
if err != nil {
return err
}
if len(name) > constellationNameLength {
return fmt.Errorf("name for constellation too long, maximum length is %d got %d: %s", constellationNameLength, len(name), name)
}
client, err := client.NewInitialized(
subscriptionID,
tenantID,
name,
location,
)
if err != nil {
return err
}
devConfigName, err := cmd.Flags().GetString("dev-config")
if err != nil {
return err
}
fileHandler := file.NewHandler(afero.NewOsFs())
config, err := config.FromFile(fileHandler, devConfigName)
if err != nil {
return err
}
return createAzure(cmd, client, fileHandler, config, size, countCoordinators, countNodes)
}
func createAzure(cmd *cobra.Command, cl azureclient, fileHandler file.Handler, config *config.Config, size string, countCoordinators, countNodes int) (retErr error) {
if err := checkDirClean(fileHandler); err != nil {
return err
}
ok, err := cmd.Flags().GetBool("yes")
if err != nil {
return err
}
if !ok {
// Ask user to confirm action.
cmd.Printf("The following Constellation will be created:\n")
cmd.Printf("%d coordinators of size %s will be created.\n", countCoordinators, size)
cmd.Printf("%d nodes of size %s will be created.\n", countNodes, size)
ok, err := askToConfirm(cmd, "Do you want to create this Constellation?")
if err != nil {
return err
}
if !ok {
cmd.Println("The creation of the Constellation was aborted.")
return nil
}
}
// Create all azure resources
defer rollbackOnError(context.Background(), cmd.OutOrStdout(), &retErr, &rollbackerAzure{client: cl})
if err := cl.CreateResourceGroup(cmd.Context()); err != nil {
return err
}
if err := cl.CreateVirtualNetwork(cmd.Context()); err != nil {
return err
}
if err := cl.CreateSecurityGroup(cmd.Context(), *config.Provider.Azure.NetworkSecurityGroupInput); err != nil {
return err
}
if err := cl.CreateInstances(cmd.Context(), client.CreateInstancesInput{
CountCoordinators: countCoordinators,
CountNodes: countNodes,
InstanceType: size,
StateDiskSizeGB: *config.StateDiskSizeGB,
Image: *config.Provider.Azure.Image,
UserAssingedIdentity: *config.Provider.Azure.UserAssignedIdentity,
}); err != nil {
return err
}
stat, err := cl.GetState()
if err != nil {
return err
}
if err := fileHandler.WriteJSON(constants.StateFilename, stat, file.OptNone); err != nil {
return err
}
cmd.Println("Your Constellation was created successfully.")
return nil
}
// createAzureCompletion handels the completion of CLI arguments. It is frequently called
// while the user types arguments of the command to suggest completion.
func createAzureCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
switch len(args) {
case 0:
return []string{}, cobra.ShellCompDirectiveNoFileComp
case 1:
return []string{}, cobra.ShellCompDirectiveNoFileComp
case 2:
return azure.InstanceTypes, cobra.ShellCompDirectiveDefault
default:
return []string{}, cobra.ShellCompDirectiveError
}
}

View File

@ -1,223 +0,0 @@
package cmd
import (
"bytes"
"errors"
"testing"
"github.com/edgelesssys/constellation/cli/azure"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/cli/file"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/internal/state"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreateAzureCmdArgumentValidation(t *testing.T) {
testCases := map[string]struct {
args []string
expectErr bool
}{
"valid create 1": {[]string{"3", "3", "Standard_DC2as_v5"}, false},
"valid create 2": {[]string{"3", "7", "Standard_DC4as_v5"}, false},
"valid create 3": {[]string{"1", "2", "Standard_DC8as_v5"}, false},
"invalid to many arguments": {[]string{"3", "2", "Standard_DC2as_v5", "Standard_DC2as_v5"}, true},
"invalid to many arguments 2": {[]string{"3", "2", "Standard_DC2as_v5", "2"}, true},
"invalid no coordinators": {[]string{"0", "1", "Standard_DC2as_v5"}, true},
"invalid no nodes": {[]string{"1", "0", "Standard_DC2as_v5"}, true},
"invalid first is no int": {[]string{"Standard_DC2as_v5", "1", "Standard_DC2as_v5"}, true},
"invalid second is no int": {[]string{"1", "Standard_DC2as_v5", "Standard_DC2as_v5"}, true},
"invalid third is no size": {[]string{"2", "2", "2"}, true},
"invalid wrong order": {[]string{"Standard_DC2as_v5", "2", "2"}, true},
}
cmd := newCreateAzureCmd()
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
err := cmd.ValidateArgs(tc.args)
if tc.expectErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}
func TestCreateAzure(t *testing.T) {
testState := state.ConstellationState{
CloudProvider: cloudprovider.Azure.String(),
AzureNodes: azure.Instances{
"0": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
"1": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
},
AzureCoordinators: azure.Instances{
"0": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
"1": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
"2": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
},
AzureResourceGroup: "resource-group",
AzureSubnet: "subnet",
AzureNetworkSecurityGroup: "network-security-group",
AzureNodesScaleSet: "nodes-scale-set",
AzureCoordinatorsScaleSet: "coordinators-scale-set",
}
someErr := errors.New("failed")
config := config.Default()
testCases := map[string]struct {
existingState *state.ConstellationState
client azureclient
interactive bool
interactiveStdin string
stateExpected state.ConstellationState
errExpected bool
}{
"create some instances": {
client: &fakeAzureClient{},
stateExpected: testState,
},
"state already exists": {
existingState: &testState,
client: &fakeAzureClient{},
errExpected: true,
},
"create some instances interactive": {
client: &fakeAzureClient{},
interactive: true,
interactiveStdin: "y\n",
stateExpected: testState,
errExpected: false,
},
"fail getState": {
client: &stubAzureClient{getStateErr: someErr},
errExpected: true,
},
"fail createVirtualNetwork": {
client: &stubAzureClient{createVirtualNetworkErr: someErr},
errExpected: true,
},
"fail createSecurityGroup": {
client: &stubAzureClient{createSecurityGroupErr: someErr},
errExpected: true,
},
"fail createInstances": {
client: &stubAzureClient{createInstancesErr: someErr},
errExpected: true,
},
"fail createResourceGroup": {
client: &stubAzureClient{createResourceGroupErr: someErr},
errExpected: true,
},
"error on rollback": {
client: &stubAzureClient{createInstancesErr: someErr, terminateResourceGroupErr: someErr},
errExpected: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
cmd := newCreateAzureCmd()
cmd.Flags().BoolP("yes", "y", false, "")
out := bytes.NewBufferString("")
cmd.SetOut(out)
errOut := bytes.NewBufferString("")
cmd.SetErr(errOut)
in := bytes.NewBufferString(tc.interactiveStdin)
cmd.SetIn(in)
if !tc.interactive {
require.NoError(cmd.Flags().Set("yes", "true")) // disable interactivity
}
fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
if tc.existingState != nil {
require.NoError(fileHandler.WriteJSON(constants.StateFilename, *tc.existingState, file.OptNone))
}
err := createAzure(cmd, tc.client, fileHandler, config, "Standard_D2s_v3", 3, 2)
if tc.errExpected {
assert.Error(err)
if stubClient, ok := tc.client.(*stubAzureClient); ok {
// Should have made a rollback on error.
assert.True(stubClient.terminateResourceGroupCalled)
}
} else {
assert.NoError(err)
var state state.ConstellationState
err := fileHandler.ReadJSON(constants.StateFilename, &state)
assert.NoError(err)
assert.Equal(tc.stateExpected, state)
}
})
}
}
func TestCreateAzureCompletion(t *testing.T) {
testCases := map[string]struct {
args []string
toComplete string
resultExpected []string
shellCDExpected cobra.ShellCompDirective
}{
"first arg": {
args: []string{},
toComplete: "21",
resultExpected: []string{},
shellCDExpected: cobra.ShellCompDirectiveNoFileComp,
},
"second arg": {
args: []string{"23"},
toComplete: "21",
resultExpected: []string{},
shellCDExpected: cobra.ShellCompDirectiveNoFileComp,
},
"third arg": {
args: []string{"23", "24"},
toComplete: "Standard_D",
resultExpected: azure.InstanceTypes,
shellCDExpected: cobra.ShellCompDirectiveDefault,
},
"fourth arg": {
args: []string{"23", "24", "Standard_D2s_v3"},
toComplete: "Standard_D",
resultExpected: []string{},
shellCDExpected: cobra.ShellCompDirectiveError,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
cmd := &cobra.Command{}
result, shellCD := createAzureCompletion(cmd, tc.args, tc.toComplete)
assert.Equal(tc.resultExpected, result)
assert.Equal(tc.shellCDExpected, shellCD)
})
}
}

View File

@ -1,139 +0,0 @@
package cmd
import (
"context"
"fmt"
"strconv"
"strings"
"github.com/edgelesssys/constellation/cli/file"
"github.com/edgelesssys/constellation/cli/gcp"
"github.com/edgelesssys/constellation/cli/gcp/client"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/constants"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
func newCreateGCPCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "gcp C_COUNT N_COUNT TYPE",
Short: "Create a Constellation of C_COUNT coordinators and N_COUNT nodes of TYPE on Google Cloud Platform.",
Long: "Create a Constellation of C_COUNT coordinators and N_COUNT nodes of TYPE on Google Cloud Platform.",
Args: cobra.MatchAll(
cobra.ExactArgs(3),
isIntGreaterZeroArg(0),
isIntGreaterZeroArg(1),
isGCPInstanceType(2),
),
ValidArgsFunction: createGCPCompletion,
RunE: runCreateGCP,
}
return cmd
}
// runCreateGCP runs the create command.
func runCreateGCP(cmd *cobra.Command, args []string) error {
countCoordinators, _ := strconv.Atoi(args[0]) // err already checked in args validation
countNodes, _ := strconv.Atoi(args[1]) // err already checked in args validation
size := strings.ToLower(args[2])
project := "constellation-331613" // TODO: This will be user input
zone := "europe-west3-b" // TODO: This will be user input
region := "europe-west3" // TODO: This will be user input
name, err := cmd.Flags().GetString("name")
if err != nil {
return err
}
if len(name) > constellationNameLength {
return fmt.Errorf("name for constellation too long, maximum length is %d got %d: %s", constellationNameLength, len(name), name)
}
client, err := client.NewInitialized(cmd.Context(), project, zone, region, name)
if err != nil {
return err
}
devConfigName, err := cmd.Flags().GetString("dev-config")
if err != nil {
return err
}
fileHandler := file.NewHandler(afero.NewOsFs())
config, err := config.FromFile(fileHandler, devConfigName)
if err != nil {
return err
}
return createGCP(cmd, client, fileHandler, config, size, countCoordinators, countNodes)
}
func createGCP(cmd *cobra.Command, cl gcpclient, fileHandler file.Handler, config *config.Config, size string, countCoordinators, countNodes int) (retErr error) {
if err := checkDirClean(fileHandler); err != nil {
return err
}
createInput := client.CreateInstancesInput{
CountNodes: countNodes,
CountCoordinators: countCoordinators,
ImageId: *config.Provider.GCP.Image,
InstanceType: size,
StateDiskSizeGB: *config.StateDiskSizeGB,
KubeEnv: gcp.KubeEnv,
DisableCVM: *config.Provider.GCP.DisableCVM,
}
ok, err := cmd.Flags().GetBool("yes")
if err != nil {
return err
}
if !ok {
// Ask user to confirm action.
cmd.Printf("The following Constellation will be created:\n")
cmd.Printf("%d coordinators and %d nodes of size %s will be created.\n", countCoordinators, countNodes, size)
ok, err := askToConfirm(cmd, "Do you want to create this Constellation?")
if err != nil {
return err
}
if !ok {
cmd.Println("The creation of the Constellation was aborted.")
return nil
}
}
// Create all gcp resources
defer rollbackOnError(context.Background(), cmd.OutOrStdout(), &retErr, &rollbackerGCP{client: cl})
if err := cl.CreateVPCs(cmd.Context(), *config.Provider.GCP.VPCsInput); err != nil {
return err
}
if err := cl.CreateFirewall(cmd.Context(), *config.Provider.GCP.FirewallInput); err != nil {
return err
}
if err := cl.CreateInstances(cmd.Context(), createInput); err != nil {
return err
}
stat, err := cl.GetState()
if err != nil {
return err
}
if err := fileHandler.WriteJSON(constants.StateFilename, stat, file.OptNone); err != nil {
return err
}
cmd.Println("Your Constellation was created successfully.")
return nil
}
// createGCPCompletion handels the completion of CLI arguments. It is frequently called
// while the user types arguments of the command to suggest completion.
func createGCPCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
switch len(args) {
case 0:
return []string{}, cobra.ShellCompDirectiveNoFileComp
case 1:
return []string{}, cobra.ShellCompDirectiveNoFileComp
case 2:
return gcp.InstanceTypes, cobra.ShellCompDirectiveDefault
default:
return []string{}, cobra.ShellCompDirectiveError
}
}

View File

@ -1,223 +0,0 @@
package cmd
import (
"bytes"
"errors"
"testing"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/cli/file"
"github.com/edgelesssys/constellation/cli/gcp"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/internal/state"
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCreateGCPCmdArgumentValidation(t *testing.T) {
testCases := map[string]struct {
args []string
expectErr bool
}{
"valid create 1": {[]string{"3", "3", "n2d-standard-2"}, false},
"valid create 2": {[]string{"3", "7", "n2d-standard-16"}, false},
"valid create 3": {[]string{"1", "2", "n2d-standard-96"}, false},
"invalid too many arguments": {[]string{"3", "2", "n2d-standard-2", "n2d-standard-2"}, true},
"invalid too many arguments 2": {[]string{"3", "2", "n2d-standard-2", "2"}, true},
"invalid no coordinators": {[]string{"0", "1", "n2d-standard-2"}, true},
"invalid no nodes": {[]string{"1", "0", "n2d-standard-2"}, true},
"invalid first is no int": {[]string{"n2d-standard-2", "1", "n2d-standard-2"}, true},
"invalid second is no int": {[]string{"3", "n2d-standard-2", "n2d-standard-2"}, true},
"invalid third is no size": {[]string{"2", "2", "2"}, true},
"invalid wrong order": {[]string{"n2d-standard-2", "2", "2"}, true},
}
cmd := newCreateGCPCmd()
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
err := cmd.ValidateArgs(tc.args)
if tc.expectErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}
func TestCreateGCP(t *testing.T) {
testState := state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: gcp.Instances{
"id-0": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
"id-1": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
},
GCPCoordinators: gcp.Instances{
"id-0": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
"id-1": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
"id-2": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
},
GCPNodeInstanceGroup: "nodes-group",
GCPCoordinatorInstanceGroup: "coordinator-group",
GCPNodeInstanceTemplate: "node-template",
GCPCoordinatorInstanceTemplate: "coordinator-template",
GCPNetwork: "network",
GCPSubnetwork: "subnetwork",
GCPFirewalls: []string{"coordinator", "wireguard", "ssh"},
}
someErr := errors.New("failed")
config := config.Default()
testCases := map[string]struct {
existingState *state.ConstellationState
client gcpclient
interactive bool
interactiveStdin string
stateExpected state.ConstellationState
errExpected bool
}{
"create some instances": {
client: &fakeGcpClient{},
stateExpected: testState,
},
"state already exists": {
existingState: &testState,
client: &fakeGcpClient{},
errExpected: true,
},
"create some instances interactive": {
client: &fakeGcpClient{},
interactive: true,
interactiveStdin: "y\n",
stateExpected: testState,
errExpected: false,
},
"fail getState": {
client: &stubGcpClient{getStateErr: someErr},
errExpected: true,
},
"fail createVPCs": {
client: &stubGcpClient{createVPCsErr: someErr},
errExpected: true,
},
"fail createFirewall": {
client: &stubGcpClient{createFirewallErr: someErr},
errExpected: true,
},
"fail createInstances": {
client: &stubGcpClient{createInstancesErr: someErr},
errExpected: true,
},
"error on rollback": {
client: &stubGcpClient{createInstancesErr: someErr, terminateVPCsErr: someErr},
errExpected: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
cmd := newCreateGCPCmd()
cmd.Flags().BoolP("yes", "y", false, "")
out := bytes.NewBufferString("")
cmd.SetOut(out)
errOut := bytes.NewBufferString("")
cmd.SetErr(errOut)
in := bytes.NewBufferString(tc.interactiveStdin)
cmd.SetIn(in)
if !tc.interactive {
require.NoError(cmd.Flags().Set("yes", "true")) // disable interactivity
}
fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
if tc.existingState != nil {
require.NoError(fileHandler.WriteJSON(constants.StateFilename, *tc.existingState, file.OptNone))
}
err := createGCP(cmd, tc.client, fileHandler, config, "n2d-standard-2", 3, 2)
if tc.errExpected {
assert.Error(err)
if stubClient, ok := tc.client.(*stubGcpClient); ok {
// Should have made a rollback on error.
assert.True(stubClient.terminateFirewallCalled)
assert.True(stubClient.terminateInstancesCalled)
assert.True(stubClient.terminateVPCsCalled)
}
} else {
assert.NoError(err)
var stat state.ConstellationState
err := fileHandler.ReadJSON(constants.StateFilename, &stat)
assert.NoError(err)
assert.Equal(tc.stateExpected, stat)
}
})
}
}
func TestCreateGCPCompletion(t *testing.T) {
testCases := map[string]struct {
args []string
toComplete string
resultExpected []string
shellCDExpected cobra.ShellCompDirective
}{
"first arg": {
args: []string{},
toComplete: "21",
resultExpected: []string{},
shellCDExpected: cobra.ShellCompDirectiveNoFileComp,
},
"second arg": {
args: []string{"23"},
toComplete: "21",
resultExpected: []string{},
shellCDExpected: cobra.ShellCompDirectiveNoFileComp,
},
"third arg": {
args: []string{"23", "24"},
toComplete: "n2d-stan",
resultExpected: gcp.InstanceTypes,
shellCDExpected: cobra.ShellCompDirectiveDefault,
},
"fourth arg": {
args: []string{"23", "24", "n2d-standard-2"},
toComplete: "n2d-stan",
resultExpected: []string{},
shellCDExpected: cobra.ShellCompDirectiveError,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
cmd := &cobra.Command{}
result, shellCD := createGCPCompletion(cmd, tc.args, tc.toComplete)
assert.Equal(tc.resultExpected, result)
assert.Equal(tc.shellCDExpected, shellCD)
})
}
}

View File

@ -1,15 +1,219 @@
package cmd package cmd
import ( import (
"bytes"
"errors"
"strings"
"testing" "testing"
"github.com/edgelesssys/constellation/cli/azure"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/cli/file" "github.com/edgelesssys/constellation/cli/file"
"github.com/edgelesssys/constellation/cli/gcp"
"github.com/edgelesssys/constellation/internal/constants" "github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/internal/state"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestCreateArgumentValidation(t *testing.T) {
testCases := map[string]struct {
args []string
wantErr bool
}{
"gcp valid create 1": {[]string{"gcp", "3", "3", "n2d-standard-2"}, false},
"gcp valid create 2": {[]string{"gcp", "3", "7", "n2d-standard-16"}, false},
"gcp valid create 3": {[]string{"gcp", "1", "2", "n2d-standard-96"}, false},
"gcp invalid too many arguments": {[]string{"gcp", "3", "2", "n2d-standard-2", "n2d-standard-2"}, true},
"gcp invalid too many arguments 2": {[]string{"gcp", "3", "2", "n2d-standard-2", "2"}, true},
"gcp invalid no coordinators": {[]string{"gcp", "0", "1", "n2d-standard-2"}, true},
"gcp invalid no nodes": {[]string{"gcp", "1", "0", "n2d-standard-2"}, true},
"gcp invalid first is no int": {[]string{"gcp", "n2d-standard-2", "1", "n2d-standard-2"}, true},
"gcp invalid second is no int": {[]string{"gcp", "3", "n2d-standard-2", "n2d-standard-2"}, true},
"gcp invalid third is no size": {[]string{"gcp", "2", "2", "2"}, true},
"gcp invalid wrong order": {[]string{"gcp", "n2d-standard-2", "2", "2"}, true},
"azure valid create 1": {[]string{"azure", "3", "3", "Standard_DC2as_v5"}, false},
"azure valid create 2": {[]string{"azure", "3", "7", "Standard_DC4as_v5"}, false},
"azure valid create 3": {[]string{"azure", "1", "2", "Standard_DC8as_v5"}, false},
"azure invalid to many arguments": {[]string{"azure", "3", "2", "Standard_DC2as_v5", "Standard_DC2as_v5"}, true},
"azure invalid to many arguments 2": {[]string{"azure", "3", "2", "Standard_DC2as_v5", "2"}, true},
"azure invalid no coordinators": {[]string{"azure", "0", "1", "Standard_DC2as_v5"}, true},
"azure invalid no nodes": {[]string{"azure", "1", "0", "Standard_DC2as_v5"}, true},
"azure invalid first is no int": {[]string{"azure", "Standard_DC2as_v5", "1", "Standard_DC2as_v5"}, true},
"azure invalid second is no int": {[]string{"azure", "1", "Standard_DC2as_v5", "Standard_DC2as_v5"}, true},
"azure invalid third is no size": {[]string{"azure", "2", "2", "2"}, true},
"azure invalid wrong order": {[]string{"azure", "Standard_DC2as_v5", "2", "2"}, true},
"aws waring": {[]string{"aws", "1", "2", "4xlarge"}, true},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
err := newCreateCmd().ValidateArgs(tc.args)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
}
})
}
}
func TestCreate(t *testing.T) {
testState := state.ConstellationState{Name: "test"}
someErr := errors.New("failed")
testCases := map[string]struct {
setupFs func(*require.Assertions) afero.Fs
creator *stubCloudCreator
provider cloudprovider.CloudProvider
yesFlag bool
devConfigFlag string
nameFlag string
stdin string
wantErr bool
wantAbbort bool
}{
"create": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
creator: &stubCloudCreator{state: testState},
provider: cloudprovider.GCP,
yesFlag: true,
},
"interactive": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
creator: &stubCloudCreator{state: testState},
provider: cloudprovider.GCP,
stdin: "yes\n",
},
"interactive abort": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
stdin: "no\n",
wantAbbort: true,
},
"interactive error": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
stdin: "foo\nfoo\nfoo\n",
wantErr: true,
},
"flag name to long": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
nameFlag: strings.Repeat("a", constellationNameLength+1),
wantErr: true,
},
"old state in directory": {
setupFs: func(require *require.Assertions) afero.Fs {
fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
require.NoError(fileHandler.Write(constants.StateFilename, []byte{1}, file.OptNone))
return fs
},
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
yesFlag: true,
wantErr: true,
},
"old adminConf in directory": {
setupFs: func(require *require.Assertions) afero.Fs {
fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
require.NoError(fileHandler.Write(constants.AdminConfFilename, []byte{1}, file.OptNone))
return fs
},
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
yesFlag: true,
wantErr: true,
},
"old masterSecret in directory": {
setupFs: func(require *require.Assertions) afero.Fs {
fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
require.NoError(fileHandler.Write(constants.MasterSecretFilename, []byte{1}, file.OptNone))
return fs
},
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
yesFlag: true,
wantErr: true,
},
"dev config does not exist": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
yesFlag: true,
devConfigFlag: "dev-config.json",
wantErr: true,
},
"create error": {
setupFs: func(require *require.Assertions) afero.Fs { return afero.NewMemMapFs() },
creator: &stubCloudCreator{createErr: someErr},
provider: cloudprovider.GCP,
yesFlag: true,
wantErr: true,
},
"write state error": {
setupFs: func(require *require.Assertions) afero.Fs {
fs := afero.NewMemMapFs()
return afero.NewReadOnlyFs(fs)
},
creator: &stubCloudCreator{},
provider: cloudprovider.GCP,
yesFlag: true,
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
cmd := newCreateCmd()
cmd.Flags().String("dev-config", "", "") // register persisten flag manually
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
cmd.SetIn(bytes.NewBufferString(tc.stdin))
if tc.yesFlag {
require.NoError(cmd.Flags().Set("yes", "true"))
}
if tc.nameFlag != "" {
require.NoError(cmd.Flags().Set("name", tc.nameFlag))
}
if tc.devConfigFlag != "" {
require.NoError(cmd.Flags().Set("dev-config", tc.devConfigFlag))
}
fileHandler := file.NewHandler(tc.setupFs(require))
err := create(cmd, tc.creator, fileHandler, 3, 3, tc.provider, "type")
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
if tc.wantAbbort {
assert.False(tc.creator.createCalled)
} else {
assert.True(tc.creator.createCalled)
var state state.ConstellationState
require.NoError(fileHandler.ReadJSON(constants.StateFilename, &state))
assert.Equal(state, testState)
}
}
})
}
}
func TestCheckDirClean(t *testing.T) { func TestCheckDirClean(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
fileHandler file.Handler fileHandler file.Handler
@ -60,3 +264,64 @@ func TestCheckDirClean(t *testing.T) {
}) })
} }
} }
func TestCreateCompletion(t *testing.T) {
testCases := map[string]struct {
args []string
wantResult []string
wantShellCD cobra.ShellCompDirective
}{
"first arg": {
args: []string{},
wantResult: []string{"aws", "gcp", "azure"},
wantShellCD: cobra.ShellCompDirectiveNoFileComp,
},
"second arg": {
args: []string{"gcp"},
wantResult: []string{},
wantShellCD: cobra.ShellCompDirectiveNoFileComp,
},
"third arg": {
args: []string{"gcp", "1"},
wantResult: []string{},
wantShellCD: cobra.ShellCompDirectiveNoFileComp,
},
"fourth arg aws": {
args: []string{"aws", "1", "2"},
wantResult: []string{
"4xlarge",
"8xlarge",
"12xlarge",
"16xlarge",
"24xlarge",
},
wantShellCD: cobra.ShellCompDirectiveNoFileComp,
},
"fourth arg gcp": {
args: []string{"gcp", "1", "2"},
wantResult: gcp.InstanceTypes,
wantShellCD: cobra.ShellCompDirectiveNoFileComp,
},
"fourth arg azure": {
args: []string{"azure", "1", "2"},
wantResult: azure.InstanceTypes,
wantShellCD: cobra.ShellCompDirectiveNoFileComp,
},
"fifth arg": {
args: []string{"aws", "1", "2", "4xlarge"},
wantResult: []string{},
wantShellCD: cobra.ShellCompDirectiveError,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
cmd := &cobra.Command{}
result, shellCD := createCompletion(cmd, tc.args, "")
assert.Equal(tc.wantResult, result)
assert.Equal(tc.wantShellCD, shellCD)
})
}
}

View File

@ -1,17 +0,0 @@
package cmd
import (
"context"
"github.com/edgelesssys/constellation/cli/ec2/client"
"github.com/edgelesssys/constellation/internal/state"
)
type ec2client interface {
GetState() (state.ConstellationState, error)
SetState(stat state.ConstellationState) error
CreateInstances(ctx context.Context, input client.CreateInput) error
TerminateInstances(ctx context.Context) error
CreateSecurityGroup(ctx context.Context, input client.SecurityGroupInput) error
DeleteSecurityGroup(ctx context.Context) error
}

View File

@ -1,139 +0,0 @@
package cmd
import (
"context"
"errors"
"strconv"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/cli/ec2"
"github.com/edgelesssys/constellation/cli/ec2/client"
"github.com/edgelesssys/constellation/internal/state"
)
type fakeEc2Client struct {
instances ec2.Instances
securityGroup string
ec2state []fakeEc2Instance
}
func (c *fakeEc2Client) GetState() (state.ConstellationState, error) {
if len(c.instances) == 0 {
return state.ConstellationState{}, errors.New("client has no instances")
}
stat := state.ConstellationState{
CloudProvider: cloudprovider.AWS.String(),
EC2Instances: c.instances,
EC2SecurityGroup: c.securityGroup,
}
for id, instance := range c.instances {
instance.PrivateIP = "192.0.2.1"
instance.PublicIP = "192.0.2.2"
c.instances[id] = instance
}
return stat, nil
}
func (c *fakeEc2Client) SetState(stat state.ConstellationState) error {
if len(stat.EC2Instances) == 0 {
return errors.New("state has no instances")
}
c.instances = stat.EC2Instances
c.securityGroup = stat.EC2SecurityGroup
return nil
}
func (c *fakeEc2Client) CreateInstances(_ context.Context, input client.CreateInput) error {
if c.securityGroup == "" {
return errors.New("client has no security group")
}
if c.instances == nil {
c.instances = make(ec2.Instances)
}
for i := 0; i < input.Count; i++ {
id := "id-" + strconv.Itoa(len(c.ec2state))
c.ec2state = append(c.ec2state, fakeEc2Instance{
state: running,
instanceID: id,
securityGroup: c.securityGroup,
tags: input.Tags,
})
c.instances[id] = ec2.Instance{}
}
return nil
}
func (c *fakeEc2Client) TerminateInstances(_ context.Context) error {
if len(c.instances) == 0 {
return nil
}
for _, instance := range c.ec2state {
instance.state = terminated
}
return nil
}
func (c *fakeEc2Client) CreateSecurityGroup(_ context.Context, input client.SecurityGroupInput) error {
if c.securityGroup != "" {
return errors.New("client already has a security group")
}
c.securityGroup = "sg-test"
return nil
}
func (c *fakeEc2Client) DeleteSecurityGroup(_ context.Context) error {
c.securityGroup = ""
return nil
}
type ec2InstanceState int
const (
running = iota
terminated
)
type fakeEc2Instance struct {
state ec2InstanceState
instanceID string
tags ec2.Tags
securityGroup string
}
type stubEc2Client struct {
terminateInstancesCalled bool
deleteSecurityGroupCalled bool
getStateErr error
setStateErr error
createInstancesErr error
terminateInstancesErr error
createSecurityGroupErr error
deleteSecurityGroupErr error
}
func (c *stubEc2Client) GetState() (state.ConstellationState, error) {
return state.ConstellationState{}, c.getStateErr
}
func (c *stubEc2Client) SetState(stat state.ConstellationState) error {
return c.setStateErr
}
func (c *stubEc2Client) CreateInstances(_ context.Context, input client.CreateInput) error {
return c.createInstancesErr
}
func (c *stubEc2Client) TerminateInstances(_ context.Context) error {
c.terminateInstancesCalled = true
return c.terminateInstancesErr
}
func (c *stubEc2Client) CreateSecurityGroup(_ context.Context, input client.SecurityGroupInput) error {
return c.createSecurityGroupErr
}
func (c *stubEc2Client) DeleteSecurityGroup(_ context.Context) error {
c.deleteSecurityGroupCalled = true
return c.deleteSecurityGroupErr
}

View File

@ -1,22 +0,0 @@
package cmd
import (
"context"
"github.com/edgelesssys/constellation/cli/gcp/client"
"github.com/edgelesssys/constellation/internal/state"
)
type gcpclient interface {
GetState() (state.ConstellationState, error)
SetState(state.ConstellationState) error
CreateVPCs(ctx context.Context, input client.VPCsInput) error
CreateFirewall(ctx context.Context, input client.FirewallInput) error
CreateInstances(ctx context.Context, input client.CreateInstancesInput) error
CreateServiceAccount(ctx context.Context, input client.ServiceAccountInput) (string, error)
TerminateFirewall(ctx context.Context) error
TerminateVPCs(context.Context) error
TerminateInstances(context.Context) error
TerminateServiceAccount(ctx context.Context) error
Close() error
}

View File

@ -1,222 +0,0 @@
package cmd
import (
"context"
"errors"
"strconv"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/cli/gcp"
"github.com/edgelesssys/constellation/cli/gcp/client"
"github.com/edgelesssys/constellation/internal/state"
)
type fakeGcpClient struct {
nodes gcp.Instances
coordinators gcp.Instances
nodesInstanceGroup string
coordinatorInstanceGroup string
coordinatorTemplate string
nodeTemplate string
network string
subnetwork string
firewalls []string
project string
uid string
name string
zone string
serviceAccount string
}
func (c *fakeGcpClient) GetState() (state.ConstellationState, error) {
stat := state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPNodes: c.nodes,
GCPCoordinators: c.coordinators,
GCPNodeInstanceGroup: c.nodesInstanceGroup,
GCPCoordinatorInstanceGroup: c.coordinatorInstanceGroup,
GCPNodeInstanceTemplate: c.nodeTemplate,
GCPCoordinatorInstanceTemplate: c.coordinatorTemplate,
GCPNetwork: c.network,
GCPSubnetwork: c.subnetwork,
GCPFirewalls: c.firewalls,
GCPProject: c.project,
Name: c.name,
UID: c.uid,
GCPZone: c.zone,
GCPServiceAccount: c.serviceAccount,
}
return stat, nil
}
func (c *fakeGcpClient) SetState(stat state.ConstellationState) error {
c.nodes = stat.GCPNodes
c.coordinators = stat.GCPCoordinators
c.nodesInstanceGroup = stat.GCPNodeInstanceGroup
c.coordinatorInstanceGroup = stat.GCPCoordinatorInstanceGroup
c.nodeTemplate = stat.GCPNodeInstanceTemplate
c.coordinatorTemplate = stat.GCPCoordinatorInstanceTemplate
c.network = stat.GCPNetwork
c.subnetwork = stat.GCPSubnetwork
c.firewalls = stat.GCPFirewalls
c.project = stat.GCPProject
c.name = stat.Name
c.uid = stat.UID
c.zone = stat.GCPZone
c.serviceAccount = stat.GCPServiceAccount
return nil
}
func (c *fakeGcpClient) CreateVPCs(ctx context.Context, input client.VPCsInput) error {
c.network = "network"
c.subnetwork = "subnetwork"
return nil
}
func (c *fakeGcpClient) CreateFirewall(ctx context.Context, input client.FirewallInput) error {
if c.network == "" {
return errors.New("client has not network")
}
var firewalls []string
for _, rule := range input.Ingress {
firewalls = append(firewalls, rule.Name)
}
c.firewalls = firewalls
return nil
}
func (c *fakeGcpClient) CreateInstances(ctx context.Context, input client.CreateInstancesInput) error {
c.coordinatorInstanceGroup = "coordinator-group"
c.nodesInstanceGroup = "nodes-group"
c.nodeTemplate = "node-template"
c.coordinatorTemplate = "coordinator-template"
c.nodes = make(gcp.Instances)
for i := 0; i < input.CountNodes; i++ {
id := "id-" + strconv.Itoa(i)
c.nodes[id] = gcp.Instance{PublicIP: "192.0.2.1", PrivateIP: "192.0.2.1"}
}
c.coordinators = make(gcp.Instances)
for i := 0; i < input.CountCoordinators; i++ {
id := "id-" + strconv.Itoa(i)
c.coordinators[id] = gcp.Instance{PublicIP: "192.0.2.1", PrivateIP: "192.0.2.1"}
}
return nil
}
func (c *fakeGcpClient) CreateServiceAccount(ctx context.Context, input client.ServiceAccountInput) (string, error) {
c.serviceAccount = "service-account@" + c.project + ".iam.gserviceaccount.com"
return client.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",
}.ConvertToCloudServiceAccountURI(), nil
}
func (c *fakeGcpClient) TerminateFirewall(ctx context.Context) error {
if len(c.firewalls) == 0 {
return nil
}
c.firewalls = nil
return nil
}
func (c *fakeGcpClient) TerminateVPCs(context.Context) error {
if len(c.firewalls) != 0 {
return errors.New("client has firewalls, which must be deleted first")
}
c.network = ""
c.subnetwork = ""
return nil
}
func (c *fakeGcpClient) TerminateInstances(context.Context) error {
c.nodeTemplate = ""
c.coordinatorTemplate = ""
c.nodesInstanceGroup = ""
c.coordinatorInstanceGroup = ""
c.nodes = nil
c.coordinators = nil
return nil
}
func (c *fakeGcpClient) TerminateServiceAccount(context.Context) error {
c.serviceAccount = ""
return nil
}
func (c *fakeGcpClient) Close() error {
return nil
}
type stubGcpClient struct {
terminateFirewallCalled bool
terminateInstancesCalled bool
terminateVPCsCalled bool
getStateErr error
setStateErr error
createVPCsErr error
createFirewallErr error
createInstancesErr error
createServiceAccountErr error
terminateFirewallErr error
terminateVPCsErr error
terminateInstancesErr error
terminateServiceAccountErr error
closeErr error
}
func (c *stubGcpClient) GetState() (state.ConstellationState, error) {
return state.ConstellationState{}, c.getStateErr
}
func (c *stubGcpClient) SetState(state.ConstellationState) error {
return c.setStateErr
}
func (c *stubGcpClient) CreateVPCs(ctx context.Context, input client.VPCsInput) error {
return c.createVPCsErr
}
func (c *stubGcpClient) CreateFirewall(ctx context.Context, input client.FirewallInput) error {
return c.createFirewallErr
}
func (c *stubGcpClient) CreateInstances(ctx context.Context, input client.CreateInstancesInput) error {
return c.createInstancesErr
}
func (c *stubGcpClient) CreateServiceAccount(ctx context.Context, input client.ServiceAccountInput) (string, error) {
return client.ServiceAccountKey{}.ConvertToCloudServiceAccountURI(), c.createServiceAccountErr
}
func (c *stubGcpClient) TerminateFirewall(ctx context.Context) error {
c.terminateFirewallCalled = true
return c.terminateFirewallErr
}
func (c *stubGcpClient) TerminateVPCs(context.Context) error {
c.terminateVPCsCalled = true
return c.terminateVPCsErr
}
func (c *stubGcpClient) TerminateInstances(context.Context) error {
c.terminateInstancesCalled = true
return c.terminateInstancesErr
}
func (c *stubGcpClient) TerminateServiceAccount(context.Context) error {
return c.terminateServiceAccountErr
}
func (c *stubGcpClient) Close() error {
return c.closeErr
}

View File

@ -11,6 +11,7 @@ import (
"text/tabwriter" "text/tabwriter"
"github.com/edgelesssys/constellation/cli/azure" "github.com/edgelesssys/constellation/cli/azure"
"github.com/edgelesssys/constellation/cli/cloud/cloudcmd"
"github.com/edgelesssys/constellation/cli/file" "github.com/edgelesssys/constellation/cli/file"
"github.com/edgelesssys/constellation/cli/gcp" "github.com/edgelesssys/constellation/cli/gcp"
"github.com/edgelesssys/constellation/cli/proto" "github.com/edgelesssys/constellation/cli/proto"
@ -63,14 +64,16 @@ func runInitialize(cmd *cobra.Command, args []string) error {
return err return err
} }
serviceAccountCreator := cloudcmd.NewServiceAccountCreator()
// We have to parse the context separately, since cmd.Context() // We have to parse the context separately, since cmd.Context()
// returns nil during the tests otherwise. // returns nil during the tests otherwise.
return initialize(cmd.Context(), cmd, protoClient, serviceAccountClient{}, fileHandler, config, status.NewWaiter(*config.Provider.GCP.PCRs), vpnHandler) return initialize(cmd.Context(), cmd, protoClient, serviceAccountCreator, fileHandler, config, status.NewWaiter(*config.Provider.GCP.PCRs), vpnHandler)
} }
// initialize initializes a Constellation. Coordinator instances are activated as Coordinators and will // initialize initializes a Constellation. Coordinator instances are activated as Coordinators and will
// themself activate the other peers as nodes. // themself activate the other peers as nodes.
func initialize(ctx context.Context, cmd *cobra.Command, protCl protoClient, serviceAccountCr serviceAccountCreator, func initialize(ctx context.Context, cmd *cobra.Command, protCl protoClient, serviceAccCreator serviceAccountCreator,
fileHandler file.Handler, config *config.Config, waiter statusWaiter, vpnHandler vpnHandler, fileHandler file.Handler, config *config.Config, waiter statusWaiter, vpnHandler vpnHandler,
) error { ) error {
flagArgs, err := evalFlagArgs(cmd, fileHandler) flagArgs, err := evalFlagArgs(cmd, fileHandler)
@ -98,7 +101,7 @@ func initialize(ctx context.Context, cmd *cobra.Command, protCl protoClient, ser
} }
cmd.Println("Creating service account ...") cmd.Println("Creating service account ...")
serviceAccount, stat, err := serviceAccountCr.createServiceAccount(ctx, stat, config) serviceAccount, stat, err := serviceAccCreator.Create(ctx, stat, config)
if err != nil { if err != nil {
return err return err
} }

View File

@ -1,95 +0,0 @@
package cmd
import (
"context"
"fmt"
azurecl "github.com/edgelesssys/constellation/cli/azure/client"
"github.com/edgelesssys/constellation/cli/cloudprovider"
ec2cl "github.com/edgelesssys/constellation/cli/ec2/client"
gcpcl "github.com/edgelesssys/constellation/cli/gcp/client"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/state"
)
type serviceAccountCreator interface {
createServiceAccount(ctx context.Context, stat state.ConstellationState, config *config.Config) (string, state.ConstellationState, error)
}
type serviceAccountClient struct{}
// createServiceAccount creates a new cloud provider service account with access to the created resources.
func (c serviceAccountClient) createServiceAccount(ctx context.Context, stat state.ConstellationState, config *config.Config) (string, state.ConstellationState, error) {
switch stat.CloudProvider {
case cloudprovider.AWS.String():
// TODO: implement
ec2client, err := ec2cl.NewFromDefault(ctx)
if err != nil {
return "", state.ConstellationState{}, err
}
return c.createServiceAccountEC2(ctx, ec2client, stat, config)
case cloudprovider.GCP.String():
gcpclient, err := gcpcl.NewFromDefault(ctx)
if err != nil {
return "", state.ConstellationState{}, err
}
serviceAccount, stat, err := c.createServiceAccountGCP(ctx, gcpclient, stat, config)
if err != nil {
return "", state.ConstellationState{}, err
}
return serviceAccount, stat, gcpclient.Close()
case cloudprovider.Azure.String():
azureclient, err := azurecl.NewFromDefault(stat.AzureSubscription, stat.AzureTenant)
if err != nil {
return "", state.ConstellationState{}, err
}
return c.createServiceAccountAzure(ctx, azureclient, stat)
}
return "", state.ConstellationState{}, fmt.Errorf("unknown cloud provider %v", stat.CloudProvider)
}
func (c serviceAccountClient) createServiceAccountAzure(ctx context.Context, cl azureclient, stat state.ConstellationState) (string, state.ConstellationState, error) {
if err := cl.SetState(stat); err != nil {
return "", state.ConstellationState{}, fmt.Errorf("failed to set state while creating service account: %w", err)
}
serviceAccount, err := cl.CreateServicePrincipal(ctx)
if err != nil {
return "", state.ConstellationState{}, fmt.Errorf("failed to create service account: %w", err)
}
stat, err = cl.GetState()
if err != nil {
return "", state.ConstellationState{}, fmt.Errorf("failed to get state after creating service account: %w", err)
}
return serviceAccount, stat, nil
}
func (c serviceAccountClient) createServiceAccountGCP(ctx context.Context, cl gcpclient, stat state.ConstellationState, config *config.Config) (string, state.ConstellationState, error) {
if err := cl.SetState(stat); err != nil {
return "", state.ConstellationState{}, fmt.Errorf("failed to set state while creating service account: %w", err)
}
input := gcpcl.ServiceAccountInput{
Roles: *config.Provider.GCP.ServiceAccountRoles,
}
serviceAccount, err := cl.CreateServiceAccount(ctx, input)
if err != nil {
return "", state.ConstellationState{}, fmt.Errorf("failed to create service account: %w", err)
}
stat, err = cl.GetState()
if err != nil {
return "", state.ConstellationState{}, fmt.Errorf("failed to get state after creating service account: %w", err)
}
return serviceAccount, stat, nil
}
//nolint:unparam
func (c serviceAccountClient) createServiceAccountEC2(ctx context.Context, cl ec2client, stat state.ConstellationState, config *config.Config) (string, state.ConstellationState, error) {
// TODO: implement
if err := cl.SetState(stat); err != nil {
return "", state.ConstellationState{}, fmt.Errorf("failed to set state while creating service account: %w", err)
}
return "", stat, nil
}

View File

@ -1,136 +0,0 @@
package cmd
import (
"context"
"errors"
"testing"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/cli/gcp"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/state"
"github.com/stretchr/testify/assert"
)
func TestCreateServiceAccountAzure(t *testing.T) {
testState := state.ConstellationState{
CloudProvider: cloudprovider.Azure.String(),
}
someErr := errors.New("failed")
testCases := map[string]struct {
existingState state.ConstellationState
client azureclient
errExpected bool
}{
"create service account works": {
existingState: testState,
client: &fakeAzureClient{},
},
"fail setState": {
existingState: testState,
client: &stubAzureClient{setStateErr: someErr},
errExpected: true,
},
"fail create": {
existingState: testState,
client: &stubAzureClient{createServicePrincipalErr: someErr},
errExpected: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
client := serviceAccountClient{}
serviceAccount, _, err := client.createServiceAccountAzure(context.Background(), tc.client, tc.existingState)
if tc.errExpected {
assert.Error(err)
} else {
assert.NoError(err)
assert.NotNil(serviceAccount)
stat, err := tc.client.GetState()
assert.NoError(err)
assert.Equal(state.ConstellationState{
CloudProvider: cloudprovider.Azure.String(),
AzureADAppObjectID: "00000000-0000-0000-0000-000000000001",
}, stat)
}
})
}
}
func TestCreateServiceAccountGCP(t *testing.T) {
testState := state.ConstellationState{
GCPProject: "project",
GCPNodes: gcp.Instances{},
GCPCoordinators: gcp.Instances{},
GCPNodeInstanceGroup: "nodes-group",
GCPCoordinatorInstanceGroup: "coordinator-group",
GCPNodeInstanceTemplate: "template",
GCPCoordinatorInstanceTemplate: "template",
GCPNetwork: "network",
GCPFirewalls: []string{},
}
config := config.Default()
someErr := errors.New("failed")
testCases := map[string]struct {
existingState state.ConstellationState
client gcpclient
errExpected bool
}{
"create service account works": {
existingState: testState,
client: &fakeGcpClient{},
},
"fail setState": {
existingState: testState,
client: &stubGcpClient{setStateErr: someErr},
errExpected: true,
},
"fail create": {
existingState: testState,
client: &stubGcpClient{createServiceAccountErr: someErr},
errExpected: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
client := serviceAccountClient{}
serviceAccount, _, err := client.createServiceAccountGCP(context.Background(), tc.client, tc.existingState, config)
if tc.errExpected {
assert.Error(err)
} else {
assert.NoError(err)
assert.NotNil(serviceAccount)
stat, err := tc.client.GetState()
assert.NoError(err)
assert.Equal(state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPProject: "project",
GCPNodes: gcp.Instances{},
GCPCoordinators: gcp.Instances{},
GCPNodeInstanceGroup: "nodes-group",
GCPCoordinatorInstanceGroup: "coordinator-group",
GCPNodeInstanceTemplate: "template",
GCPCoordinatorInstanceTemplate: "template",
GCPNetwork: "network",
GCPFirewalls: []string{},
GCPServiceAccount: "service-account@project.iam.gserviceaccount.com",
}, stat)
}
})
}
}
type stubServiceAccountCreator struct {
cloudServiceAccountURI string
createErr error
}
func (c *stubServiceAccountCreator) createServiceAccount(ctx context.Context, stat state.ConstellationState, config *config.Config) (string, state.ConstellationState, error) {
return c.cloudServiceAccountURI, stat, c.createErr
}

View File

@ -9,11 +9,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.uber.org/multierr" "go.uber.org/multierr"
azure "github.com/edgelesssys/constellation/cli/azure/client" "github.com/edgelesssys/constellation/cli/cloud/cloudcmd"
ec2 "github.com/edgelesssys/constellation/cli/ec2/client"
"github.com/edgelesssys/constellation/cli/file" "github.com/edgelesssys/constellation/cli/file"
gcp "github.com/edgelesssys/constellation/cli/gcp/client"
"github.com/edgelesssys/constellation/internal/config"
"github.com/edgelesssys/constellation/internal/constants" "github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/internal/state" "github.com/edgelesssys/constellation/internal/state"
) )
@ -32,18 +29,12 @@ func newTerminateCmd() *cobra.Command {
// runTerminate runs the terminate command. // runTerminate runs the terminate command.
func runTerminate(cmd *cobra.Command, args []string) error { func runTerminate(cmd *cobra.Command, args []string) error {
fileHandler := file.NewHandler(afero.NewOsFs()) fileHandler := file.NewHandler(afero.NewOsFs())
devConfigName, err := cmd.Flags().GetString("dev-config") terminator := cloudcmd.NewTerminator()
if err != nil {
return err return terminate(cmd, terminator, fileHandler)
}
config, err := config.FromFile(fileHandler, devConfigName)
if err != nil {
return err
}
return terminate(cmd, fileHandler, config)
} }
func terminate(cmd *cobra.Command, fileHandler file.Handler, config *config.Config) error { func terminate(cmd *cobra.Command, terminator cloudTerminator, fileHandler file.Handler) error {
var stat state.ConstellationState var stat state.ConstellationState
if err := fileHandler.ReadJSON(constants.StateFilename, &stat); err != nil { if err := fileHandler.ReadJSON(constants.StateFilename, &stat); err != nil {
return err return err
@ -51,35 +42,9 @@ func terminate(cmd *cobra.Command, fileHandler file.Handler, config *config.Conf
cmd.Println("Terminating ...") cmd.Println("Terminating ...")
if len(stat.EC2Instances) != 0 || stat.EC2SecurityGroup != "" { if err := terminator.Terminate(cmd.Context(), stat); err != nil {
ec2client, err := ec2.NewFromDefault(cmd.Context())
if err != nil {
return err return err
} }
if err := terminateEC2(cmd, ec2client, stat); err != nil {
return err
}
}
// TODO: improve check, also look for other resources that might need to be terminated
if len(stat.GCPNodes) != 0 {
gcpclient, err := gcp.NewFromDefault(cmd.Context())
if err != nil {
return err
}
if err := terminateGCP(cmd, gcpclient, stat); err != nil {
return err
}
}
if len(stat.AzureResourceGroup) != 0 {
azureclient, err := azure.NewFromDefault(stat.AzureSubscription, stat.AzureTenant)
if err != nil {
return err
}
if err := terminateAzure(cmd, azureclient, stat); err != nil {
return err
}
}
cmd.Println("Your Constellation was terminated successfully.") cmd.Println("Your Constellation was terminated successfully.")
@ -98,44 +63,3 @@ func terminate(cmd *cobra.Command, fileHandler file.Handler, config *config.Conf
return retErr return retErr
} }
func terminateAzure(cmd *cobra.Command, cl azureclient, stat state.ConstellationState) error {
if err := cl.SetState(stat); err != nil {
return fmt.Errorf("failed to terminate the Constellation: %w", err)
}
if err := cl.TerminateServicePrincipal(cmd.Context()); err != nil {
return err
}
return cl.TerminateResourceGroup(cmd.Context())
}
func terminateGCP(cmd *cobra.Command, cl gcpclient, stat state.ConstellationState) error {
if err := cl.SetState(stat); err != nil {
return fmt.Errorf("failed to terminate the Constellation: %w", err)
}
if err := cl.TerminateInstances(cmd.Context()); err != nil {
return err
}
if err := cl.TerminateFirewall(cmd.Context()); err != nil {
return err
}
if err := cl.TerminateVPCs(cmd.Context()); err != nil {
return err
}
return cl.TerminateServiceAccount(cmd.Context())
}
// terminateEC2 and remove the existing Constellation form the state file.
func terminateEC2(cmd *cobra.Command, cl ec2client, stat state.ConstellationState) error {
if err := cl.SetState(stat); err != nil {
return fmt.Errorf("failed to terminate the Constellation: %w", err)
}
if err := cl.TerminateInstances(cmd.Context()); err != nil {
return fmt.Errorf("failed to terminate the Constellation: %w", err)
}
return cl.DeleteSecurityGroup(cmd.Context())
}

View File

@ -5,12 +5,12 @@ import (
"errors" "errors"
"testing" "testing"
"github.com/edgelesssys/constellation/cli/azure" "github.com/edgelesssys/constellation/cli/file"
"github.com/edgelesssys/constellation/cli/cloudprovider" "github.com/edgelesssys/constellation/internal/constants"
"github.com/edgelesssys/constellation/cli/ec2"
"github.com/edgelesssys/constellation/cli/gcp"
"github.com/edgelesssys/constellation/internal/state" "github.com/edgelesssys/constellation/internal/state"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestTerminateCmdArgumentValidation(t *testing.T) { func TestTerminateCmdArgumentValidation(t *testing.T) {
@ -23,12 +23,13 @@ func TestTerminateCmdArgumentValidation(t *testing.T) {
"some other args": {[]string{"12", "2"}, true}, "some other args": {[]string{"12", "2"}, true},
} }
cmd := newTerminateCmd()
for name, tc := range testCases { for name, tc := range testCases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
cmd := newTerminateCmd()
err := cmd.ValidateArgs(tc.args) err := cmd.ValidateArgs(tc.args)
if tc.expectErr { if tc.expectErr {
assert.Error(err) assert.Error(err)
} else { } else {
@ -38,251 +39,98 @@ func TestTerminateCmdArgumentValidation(t *testing.T) {
} }
} }
func TestTerminateEC2(t *testing.T) { func TestTerminate(t *testing.T) {
testState := state.ConstellationState{ setupFs := func(require *require.Assertions, state state.ConstellationState) afero.Fs {
CloudProvider: cloudprovider.AWS.String(), fs := afero.NewMemMapFs()
EC2Instances: ec2.Instances{ fileHandler := file.NewHandler(fs)
"id-0": { require.NoError(fileHandler.Write(constants.AdminConfFilename, []byte{1, 2}, file.OptNone))
PrivateIP: "192.0.2.1", require.NoError(fileHandler.Write(constants.WGQuickConfigFilename, []byte{1, 2}, file.OptNone))
PublicIP: "192.0.2.1", require.NoError(fileHandler.WriteJSON(constants.StateFilename, state, file.OptNone))
}, return fs
"id-1": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
"id-3": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
},
EC2SecurityGroup: "sg-test",
} }
someErr := errors.New("failed") someErr := errors.New("failed")
testCases := map[string]struct { testCases := map[string]struct {
existingState state.ConstellationState state state.ConstellationState
client ec2client setupFs func(*require.Assertions, state.ConstellationState) afero.Fs
errExpected bool terminator spyCloudTerminator
wantErr bool
}{ }{
"terminate existing instances": { "success": {
existingState: testState, state: state.ConstellationState{CloudProvider: "gcp"},
client: &fakeEc2Client{}, setupFs: setupFs,
errExpected: false, terminator: &stubCloudTerminator{},
}, },
"state without instances": { "files to remove do not exist": {
existingState: state.ConstellationState{ state: state.ConstellationState{CloudProvider: "gcp"},
CloudProvider: cloudprovider.AWS.String(), setupFs: func(require *require.Assertions, state state.ConstellationState) afero.Fs {
EC2Instances: ec2.Instances{}, fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
require.NoError(fileHandler.WriteJSON(constants.StateFilename, state, file.OptNone))
return fs
}, },
client: &fakeEc2Client{}, terminator: &stubCloudTerminator{},
errExpected: true,
}, },
"fail TerminateInstances": { "terminate error": {
existingState: testState, state: state.ConstellationState{CloudProvider: "gcp"},
client: &stubEc2Client{terminateInstancesErr: someErr}, setupFs: setupFs,
errExpected: true, terminator: &stubCloudTerminator{terminateErr: someErr},
wantErr: true,
}, },
"fail DeleteSecurityGroup": { "missing state file": {
existingState: testState, state: state.ConstellationState{CloudProvider: "gcp"},
client: &stubEc2Client{deleteSecurityGroupErr: someErr}, setupFs: func(require *require.Assertions, state state.ConstellationState) afero.Fs {
errExpected: true, fs := afero.NewMemMapFs()
fileHandler := file.NewHandler(fs)
require.NoError(fileHandler.Write(constants.AdminConfFilename, []byte{1, 2}, file.OptNone))
require.NoError(fileHandler.Write(constants.WGQuickConfigFilename, []byte{1, 2}, file.OptNone))
return fs
},
terminator: &stubCloudTerminator{},
wantErr: true,
},
"remove file fails": {
state: state.ConstellationState{CloudProvider: "gcp"},
setupFs: func(require *require.Assertions, state state.ConstellationState) afero.Fs {
fs := setupFs(require, state)
return afero.NewReadOnlyFs(fs)
},
terminator: &stubCloudTerminator{},
wantErr: true,
}, },
} }
for name, tc := range testCases { for name, tc := range testCases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
require := require.New(t)
cmd := newTerminateCmd() cmd := newTerminateCmd()
out := bytes.NewBufferString("") cmd.SetOut(&bytes.Buffer{})
cmd.SetOut(out) cmd.SetErr(&bytes.Buffer{})
errOut := bytes.NewBufferString("")
cmd.SetErr(errOut)
err := terminateEC2(cmd, tc.client, tc.existingState) require.NotNil(tc.setupFs)
if tc.errExpected { fileHandler := file.NewHandler(tc.setupFs(require, tc.state))
err := terminate(cmd, tc.terminator, fileHandler)
if tc.wantErr {
assert.Error(err) assert.Error(err)
} else { } else {
assert.NoError(err) assert.NoError(err)
assert.True(tc.terminator.Called())
_, err := fileHandler.Stat(constants.StateFilename)
assert.Error(err)
_, err = fileHandler.Stat(constants.AdminConfFilename)
assert.Error(err)
_, err = fileHandler.Stat(constants.WGQuickConfigFilename)
assert.Error(err)
} }
}) })
} }
} }
func TestTerminateGCP(t *testing.T) { type spyCloudTerminator interface {
testState := state.ConstellationState{ cloudTerminator
GCPNodes: gcp.Instances{ Called() bool
"id-0": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
"id-1": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
},
GCPCoordinators: gcp.Instances{
"id-c": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
},
GCPNodeInstanceGroup: "nodes-group",
GCPCoordinatorInstanceGroup: "coordinator-group",
GCPNodeInstanceTemplate: "template",
GCPCoordinatorInstanceTemplate: "template",
GCPNetwork: "network",
GCPFirewalls: []string{"coordinator", "wireguard", "ssh"},
}
someErr := errors.New("failed")
testCases := map[string]struct {
existingState state.ConstellationState
client gcpclient
errExpected bool
}{
"terminate existing instances": {
existingState: testState,
client: &fakeGcpClient{},
},
"state without instances": {
existingState: state.ConstellationState{EC2Instances: ec2.Instances{}},
client: &fakeGcpClient{},
},
"state not found": {
existingState: testState,
client: &fakeGcpClient{},
},
"fail setState": {
existingState: testState,
client: &stubGcpClient{setStateErr: someErr},
errExpected: true,
},
"fail terminateFirewall": {
existingState: testState,
client: &stubGcpClient{terminateFirewallErr: someErr},
errExpected: true,
},
"fail terminateVPC": {
existingState: testState,
client: &stubGcpClient{terminateVPCsErr: someErr},
errExpected: true,
},
"fail terminateInstances": {
existingState: testState,
client: &stubGcpClient{terminateInstancesErr: someErr},
errExpected: true,
},
"fail terminateServiceAccount": {
existingState: testState,
client: &stubGcpClient{terminateServiceAccountErr: someErr},
errExpected: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
cmd := newTerminateCmd()
out := bytes.NewBufferString("")
cmd.SetOut(out)
errOut := bytes.NewBufferString("")
cmd.SetErr(errOut)
err := terminateGCP(cmd, tc.client, tc.existingState)
if tc.errExpected {
assert.Error(err)
} else {
assert.NoError(err)
stat, err := tc.client.GetState()
assert.NoError(err)
assert.Equal(state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
}, stat)
}
})
}
}
func TestTerminateAzure(t *testing.T) {
testState := state.ConstellationState{
CloudProvider: cloudprovider.Azure.String(),
AzureNodes: azure.Instances{
"id-0": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
"id-1": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
},
AzureCoordinators: azure.Instances{
"id-c": {
PrivateIP: "192.0.2.1",
PublicIP: "192.0.2.1",
},
},
AzureResourceGroup: "test",
}
someErr := errors.New("failed")
testCases := map[string]struct {
existingState state.ConstellationState
client azureclient
errExpected bool
}{
"terminate existing instances": {
existingState: testState,
client: &fakeAzureClient{},
},
"state resource group": {
existingState: state.ConstellationState{AzureResourceGroup: ""},
client: &fakeAzureClient{},
},
"state not found": {
existingState: testState,
client: &fakeAzureClient{},
},
"fail setState": {
existingState: testState,
client: &stubAzureClient{setStateErr: someErr},
errExpected: true,
},
"fail resource group termination": {
existingState: testState,
client: &stubAzureClient{terminateResourceGroupErr: someErr},
errExpected: true,
},
"fail service principal termination": {
existingState: testState,
client: &stubAzureClient{terminateServicePrincipalErr: someErr},
errExpected: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
cmd := newTerminateCmd()
out := bytes.NewBufferString("")
cmd.SetOut(out)
errOut := bytes.NewBufferString("")
cmd.SetErr(errOut)
err := terminateAzure(cmd, tc.client, tc.existingState)
if tc.errExpected {
assert.Error(err)
} else {
assert.NoError(err)
stat, err := tc.client.GetState()
assert.NoError(err)
assert.Equal(state.ConstellationState{
CloudProvider: cloudprovider.Azure.String(),
}, stat)
}
})
}
} }

View File

@ -46,10 +46,9 @@ func TestAskToConfirm(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)
out := bytes.NewBufferString("") out := &bytes.Buffer{}
cmd.SetOut(out) cmd.SetOut(out)
errOut := bytes.NewBufferString("") cmd.SetErr(&bytes.Buffer{})
cmd.SetErr(errOut)
in := bytes.NewBufferString(tc.input) in := bytes.NewBufferString(tc.input)
cmd.SetIn(in) cmd.SetIn(in)

View File

@ -1,11 +1,13 @@
package cmd package cmd
import ( import (
"errors"
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"github.com/edgelesssys/constellation/cli/azure" "github.com/edgelesssys/constellation/cli/azure"
"github.com/edgelesssys/constellation/cli/cloudprovider"
"github.com/edgelesssys/constellation/cli/ec2" "github.com/edgelesssys/constellation/cli/ec2"
"github.com/edgelesssys/constellation/cli/gcp" "github.com/edgelesssys/constellation/cli/gcp"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -31,21 +33,11 @@ func isIntGreaterArg(arg int, i int) cobra.PositionalArgs {
}) })
} }
// isValidAWSCoordinatorCount checks additional conditions for the AWS coordinator count. // warnAWS warns that AWS isn't supported.
func isValidAWSCoordinatorCount(coordCountPos, providerPos int) cobra.PositionalArgs { func warnAWS(providerPos int) cobra.PositionalArgs {
return func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error {
if strings.ToLower(args[providerPos]) != "aws" { if cloudprovider.FromString(args[providerPos]) == cloudprovider.AWS {
return nil return errors.New("AWS isn't supported")
}
v, err := strconv.Atoi(args[coordCountPos])
if err != nil {
return fmt.Errorf("argument %d must be an integer", coordCountPos)
}
if v != 1 {
return fmt.Errorf(
"argument %d is %d, invalid coordinator count for AWS, has to be 1",
coordCountPos, v,
)
} }
return nil return nil
} }
@ -110,15 +102,13 @@ func isInstanceTypeForProvider(typePos, providerPos int) cobra.PositionalArgs {
) )
} }
switch strings.ToLower(args[providerPos]) { switch cloudprovider.FromString(args[providerPos]) {
case "aws": case cloudprovider.GCP:
return isEC2InstanceType(typePos)(cmd, args)
case "gcp":
return isGCPInstanceType(typePos)(cmd, args) return isGCPInstanceType(typePos)(cmd, args)
case "azure": case cloudprovider.Azure:
return isAzureInstanceType(typePos)(cmd, args) return isAzureInstanceType(typePos)(cmd, args)
default: default:
return fmt.Errorf("argument %s isn't a valid cloud platform", args[0]) return fmt.Errorf("argument %s isn't a valid cloud platform", args[providerPos])
} }
} }
} }

View File

@ -191,15 +191,12 @@ func TestIsInstanceTypeForProvider(t *testing.T) {
args []string args []string
expectErr bool expectErr bool
}{ }{
"valid ec2 type 1": {1, 0, []string{"aws", "4xl"}, false},
"valid ec2 type 2": {1, 0, []string{"aws", "12xlarge", "foo"}, false},
"valid gcp type 1": {1, 0, []string{"gcp", "n2d-standard-4"}, false}, "valid gcp type 1": {1, 0, []string{"gcp", "n2d-standard-4"}, false},
"valid gcp type 2": {1, 0, []string{"gcp", "n2d-standard-16", "foo"}, false}, "valid gcp type 2": {1, 0, []string{"gcp", "n2d-standard-16", "foo"}, false},
"valid azure type 1": {1, 0, []string{"azure", "Standard_DC2as_v5"}, false}, "valid azure type 1": {1, 0, []string{"azure", "Standard_DC2as_v5"}, false},
"valid azure type 2": {1, 0, []string{"azure", "Standard_DC8as_v5", "foo"}, false}, "valid azure type 2": {1, 0, []string{"azure", "Standard_DC8as_v5", "foo"}, false},
"mixed order 1": {0, 3, []string{"4xl", "", "foo", "aws"}, false}, "mixed order 1": {0, 3, []string{"n2d-standard-4", "", "foo", "gcp"}, false},
"mixed order 2": {2, 1, []string{"", "gcp", "n2d-standard-4", "foo", "bar"}, false}, "mixed order 2": {2, 1, []string{"", "gcp", "n2d-standard-4", "foo", "bar"}, false},
"invalid ec2 type": {1, 0, []string{"aws", "foo"}, true},
"invalid gcp type": {1, 0, []string{"gcp", "foo"}, true}, "invalid gcp type": {1, 0, []string{"gcp", "foo"}, true},
"invalid azure type": {1, 0, []string{"azure", "foo"}, true}, "invalid azure type": {1, 0, []string{"azure", "foo"}, true},
"args to short": {2, 0, []string{"foo"}, true}, "args to short": {2, 0, []string{"foo"}, true},

View File

@ -13,7 +13,7 @@ func TestVersionCmd(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
cmd := newVersionCmd() cmd := newVersionCmd()
b := bytes.NewBufferString("") b := &bytes.Buffer{}
cmd.SetOut(b) cmd.SetOut(b)
err := cmd.Execute() err := cmd.Execute()

View File

@ -186,7 +186,7 @@ func TestPrintLogStream(t *testing.T) {
}, },
} }
client := NewActivationRespClient(respClient) client := NewActivationRespClient(respClient)
out := bytes.NewBufferString("") out := &bytes.Buffer{}
assert.NoError(client.WriteLogStream(out)) assert.NoError(client.WriteLogStream(out))
assert.Equal(out.Len(), 10*11) // 10 messages * (len(message) + 1 newline) assert.Equal(out.Len(), 10*11) // 10 messages * (len(message) + 1 newline)
@ -198,8 +198,7 @@ func TestPrintLogStream(t *testing.T) {
recvErr: someErr, recvErr: someErr,
} }
client = NewActivationRespClient(respClient) client = NewActivationRespClient(respClient)
out = bytes.NewBufferString("") assert.Error(client.WriteLogStream(&bytes.Buffer{}))
assert.Error(client.WriteLogStream(out))
} }
func TestGetKubeconfig(t *testing.T) { func TestGetKubeconfig(t *testing.T) {