mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-02-22 07:50:04 -05:00
Split cmd package
This commit is contained in:
parent
63898c42bf
commit
de52bf14da
37
cli/cloud/cloudcmd/clients.go
Normal file
37
cli/cloud/cloudcmd/clients.go
Normal 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
|
||||
}
|
419
cli/cloud/cloudcmd/clients_test.go
Normal file
419
cli/cloud/cloudcmd/clients_test.go
Normal 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
|
||||
}
|
123
cli/cloud/cloudcmd/create.go
Normal file
123
cli/cloud/cloudcmd/create.go
Normal 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()
|
||||
}
|
187
cli/cloud/cloudcmd/create_test.go
Normal file
187
cli/cloud/cloudcmd/create_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package cmd
|
||||
package cloudcmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -47,14 +47,3 @@ type rollbackerAzure struct {
|
||||
func (r *rollbackerAzure) rollback(ctx context.Context) error {
|
||||
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
|
||||
}
|
||||
|
104
cli/cloud/cloudcmd/serviceaccount.go
Normal file
104
cli/cloud/cloudcmd/serviceaccount.go
Normal 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
|
||||
}
|
157
cli/cloud/cloudcmd/serviceaccount_test.go
Normal file
157
cli/cloud/cloudcmd/serviceaccount_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
79
cli/cloud/cloudcmd/terminate.go
Normal file
79
cli/cloud/cloudcmd/terminate.go
Normal 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)
|
||||
}
|
159
cli/cloud/cloudcmd/terminate_test.go
Normal file
159
cli/cloud/cloudcmd/terminate_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
package cloudprovider
|
||||
|
||||
import "strings"
|
||||
|
||||
//go:generate stringer -type=CloudProvider
|
||||
|
||||
// CloudProvider is cloud provider used by the CLI.
|
||||
@ -11,3 +13,18 @@ const (
|
||||
Azure
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
28
cli/cmd/cloud.go
Normal 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
49
cli/cmd/cloud_test.go
Normal 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
|
||||
}
|
@ -4,27 +4,134 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"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/gcp"
|
||||
"github.com/edgelesssys/constellation/internal/config"
|
||||
"github.com/edgelesssys/constellation/internal/constants"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func newCreateCmd() *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.",
|
||||
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.PersistentFlags().BoolP("yes", "y", false, "Set this flag to create the Constellation without further confirmation.")
|
||||
cmd.Flags().String("name", "constell", "Set this flag to create the Constellation with the specified name.")
|
||||
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
|
||||
}
|
||||
|
||||
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.
|
||||
func checkDirClean(fileHandler file.Handler) error {
|
||||
if _, err := fileHandler.Stat(constants.StateFilename); !errors.Is(err, fs.ErrNotExist) {
|
||||
@ -39,3 +146,35 @@ func checkDirClean(fileHandler file.Handler) error {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -1,15 +1,219 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/cli/azure"
|
||||
"github.com/edgelesssys/constellation/cli/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/cli/file"
|
||||
"github.com/edgelesssys/constellation/cli/gcp"
|
||||
"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 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) {
|
||||
testCases := map[string]struct {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -11,6 +11,7 @@ import (
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/edgelesssys/constellation/cli/azure"
|
||||
"github.com/edgelesssys/constellation/cli/cloud/cloudcmd"
|
||||
"github.com/edgelesssys/constellation/cli/file"
|
||||
"github.com/edgelesssys/constellation/cli/gcp"
|
||||
"github.com/edgelesssys/constellation/cli/proto"
|
||||
@ -63,14 +64,16 @@ func runInitialize(cmd *cobra.Command, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
serviceAccountCreator := cloudcmd.NewServiceAccountCreator()
|
||||
|
||||
// We have to parse the context separately, since cmd.Context()
|
||||
// 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
|
||||
// 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,
|
||||
) error {
|
||||
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 ...")
|
||||
serviceAccount, stat, err := serviceAccountCr.createServiceAccount(ctx, stat, config)
|
||||
serviceAccount, stat, err := serviceAccCreator.Create(ctx, stat, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -9,11 +9,8 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/multierr"
|
||||
|
||||
azure "github.com/edgelesssys/constellation/cli/azure/client"
|
||||
ec2 "github.com/edgelesssys/constellation/cli/ec2/client"
|
||||
"github.com/edgelesssys/constellation/cli/cloud/cloudcmd"
|
||||
"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/state"
|
||||
)
|
||||
@ -32,18 +29,12 @@ func newTerminateCmd() *cobra.Command {
|
||||
// runTerminate runs the terminate command.
|
||||
func runTerminate(cmd *cobra.Command, args []string) error {
|
||||
fileHandler := file.NewHandler(afero.NewOsFs())
|
||||
devConfigName, err := cmd.Flags().GetString("dev-config")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config, err := config.FromFile(fileHandler, devConfigName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return terminate(cmd, fileHandler, config)
|
||||
terminator := cloudcmd.NewTerminator()
|
||||
|
||||
return terminate(cmd, terminator, fileHandler)
|
||||
}
|
||||
|
||||
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
|
||||
if err := fileHandler.ReadJSON(constants.StateFilename, &stat); err != nil {
|
||||
return err
|
||||
@ -51,34 +42,8 @@ func terminate(cmd *cobra.Command, fileHandler file.Handler, config *config.Conf
|
||||
|
||||
cmd.Println("Terminating ...")
|
||||
|
||||
if len(stat.EC2Instances) != 0 || stat.EC2SecurityGroup != "" {
|
||||
ec2client, err := ec2.NewFromDefault(cmd.Context())
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
if err := terminator.Terminate(cmd.Context(), stat); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.Println("Your Constellation was terminated successfully.")
|
||||
@ -98,44 +63,3 @@ func terminate(cmd *cobra.Command, fileHandler file.Handler, config *config.Conf
|
||||
|
||||
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())
|
||||
}
|
||||
|
@ -5,12 +5,12 @@ import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/edgelesssys/constellation/cli/azure"
|
||||
"github.com/edgelesssys/constellation/cli/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/cli/ec2"
|
||||
"github.com/edgelesssys/constellation/cli/gcp"
|
||||
"github.com/edgelesssys/constellation/cli/file"
|
||||
"github.com/edgelesssys/constellation/internal/constants"
|
||||
"github.com/edgelesssys/constellation/internal/state"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestTerminateCmdArgumentValidation(t *testing.T) {
|
||||
@ -23,12 +23,13 @@ func TestTerminateCmdArgumentValidation(t *testing.T) {
|
||||
"some other args": {[]string{"12", "2"}, true},
|
||||
}
|
||||
|
||||
cmd := newTerminateCmd()
|
||||
|
||||
for name, tc := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
cmd := newTerminateCmd()
|
||||
err := cmd.ValidateArgs(tc.args)
|
||||
|
||||
if tc.expectErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
@ -38,251 +39,98 @@ func TestTerminateCmdArgumentValidation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTerminateEC2(t *testing.T) {
|
||||
testState := state.ConstellationState{
|
||||
CloudProvider: cloudprovider.AWS.String(),
|
||||
EC2Instances: ec2.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-3": {
|
||||
PrivateIP: "192.0.2.1",
|
||||
PublicIP: "192.0.2.1",
|
||||
},
|
||||
},
|
||||
EC2SecurityGroup: "sg-test",
|
||||
func TestTerminate(t *testing.T) {
|
||||
setupFs := func(require *require.Assertions, state state.ConstellationState) afero.Fs {
|
||||
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))
|
||||
require.NoError(fileHandler.WriteJSON(constants.StateFilename, state, file.OptNone))
|
||||
return fs
|
||||
}
|
||||
someErr := errors.New("failed")
|
||||
|
||||
testCases := map[string]struct {
|
||||
existingState state.ConstellationState
|
||||
client ec2client
|
||||
errExpected bool
|
||||
state state.ConstellationState
|
||||
setupFs func(*require.Assertions, state.ConstellationState) afero.Fs
|
||||
terminator spyCloudTerminator
|
||||
wantErr bool
|
||||
}{
|
||||
"terminate existing instances": {
|
||||
existingState: testState,
|
||||
client: &fakeEc2Client{},
|
||||
errExpected: false,
|
||||
"success": {
|
||||
state: state.ConstellationState{CloudProvider: "gcp"},
|
||||
setupFs: setupFs,
|
||||
terminator: &stubCloudTerminator{},
|
||||
},
|
||||
"state without instances": {
|
||||
existingState: state.ConstellationState{
|
||||
CloudProvider: cloudprovider.AWS.String(),
|
||||
EC2Instances: ec2.Instances{},
|
||||
"files to remove do not exist": {
|
||||
state: state.ConstellationState{CloudProvider: "gcp"},
|
||||
setupFs: func(require *require.Assertions, state state.ConstellationState) afero.Fs {
|
||||
fs := afero.NewMemMapFs()
|
||||
fileHandler := file.NewHandler(fs)
|
||||
require.NoError(fileHandler.WriteJSON(constants.StateFilename, state, file.OptNone))
|
||||
return fs
|
||||
},
|
||||
client: &fakeEc2Client{},
|
||||
errExpected: true,
|
||||
terminator: &stubCloudTerminator{},
|
||||
},
|
||||
"fail TerminateInstances": {
|
||||
existingState: testState,
|
||||
client: &stubEc2Client{terminateInstancesErr: someErr},
|
||||
errExpected: true,
|
||||
"terminate error": {
|
||||
state: state.ConstellationState{CloudProvider: "gcp"},
|
||||
setupFs: setupFs,
|
||||
terminator: &stubCloudTerminator{terminateErr: someErr},
|
||||
wantErr: true,
|
||||
},
|
||||
"fail DeleteSecurityGroup": {
|
||||
existingState: testState,
|
||||
client: &stubEc2Client{deleteSecurityGroupErr: someErr},
|
||||
errExpected: true,
|
||||
"missing state file": {
|
||||
state: state.ConstellationState{CloudProvider: "gcp"},
|
||||
setupFs: func(require *require.Assertions, state state.ConstellationState) afero.Fs {
|
||||
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 {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
require := require.New(t)
|
||||
|
||||
cmd := newTerminateCmd()
|
||||
out := bytes.NewBufferString("")
|
||||
cmd.SetOut(out)
|
||||
errOut := bytes.NewBufferString("")
|
||||
cmd.SetErr(errOut)
|
||||
cmd.SetOut(&bytes.Buffer{})
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
|
||||
err := terminateEC2(cmd, tc.client, tc.existingState)
|
||||
if tc.errExpected {
|
||||
require.NotNil(tc.setupFs)
|
||||
fileHandler := file.NewHandler(tc.setupFs(require, tc.state))
|
||||
|
||||
err := terminate(cmd, tc.terminator, fileHandler)
|
||||
|
||||
if tc.wantErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
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) {
|
||||
testState := state.ConstellationState{
|
||||
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: "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)
|
||||
}
|
||||
})
|
||||
}
|
||||
type spyCloudTerminator interface {
|
||||
cloudTerminator
|
||||
Called() bool
|
||||
}
|
||||
|
@ -46,10 +46,9 @@ func TestAskToConfirm(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
out := bytes.NewBufferString("")
|
||||
out := &bytes.Buffer{}
|
||||
cmd.SetOut(out)
|
||||
errOut := bytes.NewBufferString("")
|
||||
cmd.SetErr(errOut)
|
||||
cmd.SetErr(&bytes.Buffer{})
|
||||
in := bytes.NewBufferString(tc.input)
|
||||
cmd.SetIn(in)
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/edgelesssys/constellation/cli/azure"
|
||||
"github.com/edgelesssys/constellation/cli/cloudprovider"
|
||||
"github.com/edgelesssys/constellation/cli/ec2"
|
||||
"github.com/edgelesssys/constellation/cli/gcp"
|
||||
"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.
|
||||
func isValidAWSCoordinatorCount(coordCountPos, providerPos int) cobra.PositionalArgs {
|
||||
// warnAWS warns that AWS isn't supported.
|
||||
func warnAWS(providerPos int) cobra.PositionalArgs {
|
||||
return func(cmd *cobra.Command, args []string) error {
|
||||
if strings.ToLower(args[providerPos]) != "aws" {
|
||||
return nil
|
||||
}
|
||||
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,
|
||||
)
|
||||
if cloudprovider.FromString(args[providerPos]) == cloudprovider.AWS {
|
||||
return errors.New("AWS isn't supported")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -110,15 +102,13 @@ func isInstanceTypeForProvider(typePos, providerPos int) cobra.PositionalArgs {
|
||||
)
|
||||
}
|
||||
|
||||
switch strings.ToLower(args[providerPos]) {
|
||||
case "aws":
|
||||
return isEC2InstanceType(typePos)(cmd, args)
|
||||
case "gcp":
|
||||
switch cloudprovider.FromString(args[providerPos]) {
|
||||
case cloudprovider.GCP:
|
||||
return isGCPInstanceType(typePos)(cmd, args)
|
||||
case "azure":
|
||||
case cloudprovider.Azure:
|
||||
return isAzureInstanceType(typePos)(cmd, args)
|
||||
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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -191,15 +191,12 @@ func TestIsInstanceTypeForProvider(t *testing.T) {
|
||||
args []string
|
||||
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 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 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},
|
||||
"invalid ec2 type": {1, 0, []string{"aws", "foo"}, true},
|
||||
"invalid gcp type": {1, 0, []string{"gcp", "foo"}, true},
|
||||
"invalid azure type": {1, 0, []string{"azure", "foo"}, true},
|
||||
"args to short": {2, 0, []string{"foo"}, true},
|
||||
|
@ -13,7 +13,7 @@ func TestVersionCmd(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
cmd := newVersionCmd()
|
||||
b := bytes.NewBufferString("")
|
||||
b := &bytes.Buffer{}
|
||||
cmd.SetOut(b)
|
||||
|
||||
err := cmd.Execute()
|
||||
|
@ -186,7 +186,7 @@ func TestPrintLogStream(t *testing.T) {
|
||||
},
|
||||
}
|
||||
client := NewActivationRespClient(respClient)
|
||||
out := bytes.NewBufferString("")
|
||||
out := &bytes.Buffer{}
|
||||
assert.NoError(client.WriteLogStream(out))
|
||||
assert.Equal(out.Len(), 10*11) // 10 messages * (len(message) + 1 newline)
|
||||
|
||||
@ -198,8 +198,7 @@ func TestPrintLogStream(t *testing.T) {
|
||||
recvErr: someErr,
|
||||
}
|
||||
client = NewActivationRespClient(respClient)
|
||||
out = bytes.NewBufferString("")
|
||||
assert.Error(client.WriteLogStream(out))
|
||||
assert.Error(client.WriteLogStream(&bytes.Buffer{}))
|
||||
}
|
||||
|
||||
func TestGetKubeconfig(t *testing.T) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user