Use Terraform for create on GCP

This commit is contained in:
katexochen 2022-09-27 09:22:29 +02:00 committed by Paul Meyer
parent f990c4d692
commit d973740b03
25 changed files with 341 additions and 607 deletions

View file

@ -10,23 +10,16 @@ import (
"context"
azurecl "github.com/edgelesssys/constellation/v2/cli/internal/azure/client"
gcpcl "github.com/edgelesssys/constellation/v2/cli/internal/gcp/client"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/state"
)
type gcpclient interface {
type terraformClient interface {
GetState() state.ConstellationState
SetState(state.ConstellationState)
CreateVPCs(ctx context.Context) error
CreateFirewall(ctx context.Context, input gcpcl.FirewallInput) error
CreateInstances(ctx context.Context, input gcpcl.CreateInstancesInput) error
CreateLoadBalancers(ctx context.Context, isDebugCluster bool) error
TerminateFirewall(ctx context.Context) error
TerminateVPCs(context.Context) error
TerminateLoadBalancers(context.Context) error
TerminateInstances(context.Context) error
Close() error
CreateCluster(ctx context.Context, name string, input terraform.Variables) error
DestroyCluster(ctx context.Context) error
CleanUpWorkspace() error
RemoveInstaller()
}
type azureclient interface {
@ -39,11 +32,3 @@ type azureclient interface {
CreateInstances(ctx context.Context, input azurecl.CreateInstancesInput) error
TerminateResourceGroupResources(ctx context.Context) error
}
type qemuclient interface {
GetState() state.ConstellationState
CreateCluster(ctx context.Context, name string, input terraform.CreateClusterInput) error
DestroyCluster(ctx context.Context) error
CleanUpWorkspace() error
RemoveInstaller()
}

View file

@ -8,12 +8,11 @@ package cloudcmd
import (
"context"
"errors"
"strconv"
"testing"
azurecl "github.com/edgelesssys/constellation/v2/cli/internal/azure/client"
gcpcl "github.com/edgelesssys/constellation/v2/cli/internal/gcp/client"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/azureshared"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudtypes"
@ -195,196 +194,34 @@ func (c *stubAzureClient) TerminateServicePrincipal(ctx context.Context) error {
return c.terminateServicePrincipalErr
}
type fakeGcpClient struct {
workers cloudtypes.Instances
controlPlanes cloudtypes.Instances
workerInstanceGroup string
controlPlaneInstanceGroup string
controlPlaneTemplate string
workerTemplate string
network string
subnetwork string
firewalls []string
project string
uid string
name string
zone string
loadbalancers []string
type stubTerraformClient struct {
state state.ConstellationState
cleanUpWorkspaceCalled bool
removeInstallerCalled bool
destroyClusterCalled bool
createClusterErr error
destroyClusterErr error
cleanUpWorkspaceErr error
}
func (c *fakeGcpClient) GetState() state.ConstellationState {
return state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPWorkerInstances: c.workers,
GCPControlPlaneInstances: c.controlPlanes,
GCPWorkerInstanceGroup: c.workerInstanceGroup,
GCPControlPlaneInstanceGroup: c.controlPlaneInstanceGroup,
GCPWorkerInstanceTemplate: c.workerTemplate,
GCPControlPlaneInstanceTemplate: c.controlPlaneTemplate,
GCPNetwork: c.network,
GCPSubnetwork: c.subnetwork,
GCPFirewalls: c.firewalls,
GCPProject: c.project,
Name: c.name,
UID: c.uid,
GCPZone: c.zone,
GCPLoadbalancers: c.loadbalancers,
}
func (c *stubTerraformClient) GetState() state.ConstellationState {
return c.state
}
func (c *fakeGcpClient) SetState(stat state.ConstellationState) {
c.workers = stat.GCPWorkerInstances
c.controlPlanes = stat.GCPControlPlaneInstances
c.workerInstanceGroup = stat.GCPWorkerInstanceGroup
c.controlPlaneInstanceGroup = stat.GCPControlPlaneInstanceGroup
c.workerTemplate = stat.GCPWorkerInstanceTemplate
c.controlPlaneTemplate = stat.GCPControlPlaneInstanceTemplate
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.loadbalancers = stat.GCPLoadbalancers
func (c *stubTerraformClient) CreateCluster(ctx context.Context, name string, input terraform.Variables) error {
return c.createClusterErr
}
func (c *fakeGcpClient) CreateVPCs(ctx context.Context) error {
c.network = "network"
c.subnetwork = "subnetwork"
return nil
func (c *stubTerraformClient) DestroyCluster(ctx context.Context) error {
c.destroyClusterCalled = true
return c.destroyClusterErr
}
func (c *fakeGcpClient) CreateFirewall(ctx context.Context, input gcpcl.FirewallInput) error {
if c.network == "" {
return errors.New("client has not network")
}
for _, rule := range input.Ingress {
c.firewalls = append(c.firewalls, rule.Name)
}
return nil
func (c *stubTerraformClient) CleanUpWorkspace() error {
c.cleanUpWorkspaceCalled = true
return c.cleanUpWorkspaceErr
}
func (c *fakeGcpClient) CreateInstances(ctx context.Context, input gcpcl.CreateInstancesInput) error {
c.controlPlaneInstanceGroup = "controlplane-group"
c.workerInstanceGroup = "workers-group"
c.workerTemplate = "worker-template"
c.controlPlaneTemplate = "controlplane-template"
c.workers = make(cloudtypes.Instances)
for i := 0; i < input.CountWorkers; i++ {
id := "id-" + strconv.Itoa(i)
c.workers[id] = cloudtypes.Instance{PublicIP: "192.0.2.1", PrivateIP: "192.0.2.1"}
}
c.controlPlanes = make(cloudtypes.Instances)
for i := 0; i < input.CountControlPlanes; i++ {
id := "id-" + strconv.Itoa(i)
c.controlPlanes[id] = cloudtypes.Instance{PublicIP: "192.0.2.1", PrivateIP: "192.0.2.1"}
}
return nil
}
func (c *fakeGcpClient) CreateLoadBalancers(ctx context.Context, isDebugCluster bool) error {
c.loadbalancers = []string{"kube-lb", "boot-lb", "verify-lb"}
return 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.workerTemplate = ""
c.controlPlaneTemplate = ""
c.workerInstanceGroup = ""
c.controlPlaneInstanceGroup = ""
c.workers = nil
c.controlPlanes = nil
return nil
}
func (c *fakeGcpClient) TerminateLoadBalancers(context.Context) error {
c.loadbalancers = nil
return nil
}
func (c *fakeGcpClient) Close() error {
return nil
}
type stubGcpClient struct {
terminateFirewallCalled bool
terminateInstancesCalled bool
terminateVPCsCalled bool
closeCalled bool
createVPCsErr error
createFirewallErr error
createInstancesErr error
createLoadBalancerErr error
terminateFirewallErr error
terminateVPCsErr error
terminateInstancesErr error
terminateLoadBalancerErr error
closeErr error
}
func (c *stubGcpClient) GetState() state.ConstellationState {
return state.ConstellationState{}
}
func (c *stubGcpClient) SetState(state.ConstellationState) {
}
func (c *stubGcpClient) CreateVPCs(ctx context.Context) 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) CreateLoadBalancers(ctx context.Context, isDebugClient bool) error {
return c.createLoadBalancerErr
}
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) TerminateLoadBalancers(context.Context) error {
return c.terminateLoadBalancerErr
}
func (c *stubGcpClient) Close() error {
c.closeCalled = true
return c.closeErr
func (c *stubTerraformClient) RemoveInstaller() {
c.removeInstallerCalled = true
}

View file

@ -13,8 +13,6 @@ import (
"runtime"
azurecl "github.com/edgelesssys/constellation/v2/cli/internal/azure/client"
"github.com/edgelesssys/constellation/v2/cli/internal/gcp"
gcpcl "github.com/edgelesssys/constellation/v2/cli/internal/gcp/client"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudtypes"
@ -25,28 +23,21 @@ import (
// 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, resourceGroup string) (azureclient, error)
newQEMUClient func(ctx context.Context) (qemuclient, error)
out io.Writer
newTerraformClient func(ctx context.Context, provider cloudprovider.Provider) (terraformClient, error)
newAzureClient func(subscriptionID, tenantID, name, location, resourceGroup 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)
newTerraformClient: func(ctx context.Context, provider cloudprovider.Provider) (terraformClient, error) {
return terraform.New(ctx, provider)
},
newAzureClient: func(subscriptionID, tenantID, name, location, resourceGroup string) (azureclient, error) {
return azurecl.NewInitialized(subscriptionID, tenantID, name, location, resourceGroup)
},
newQEMUClient: func(ctx context.Context) (qemuclient, error) {
if runtime.GOARCH != "amd64" || runtime.GOOS != "linux" {
return nil, fmt.Errorf("creation of a QEMU based Constellation is not supported for %s/%s", runtime.GOOS, runtime.GOARCH)
}
return terraform.New(ctx, cloudprovider.QEMU)
},
}
}
@ -63,18 +54,12 @@ func (c *Creator) Create(ctx context.Context, provider cloudprovider.Provider, c
switch provider {
case cloudprovider.GCP:
cl, err := c.newGCPClient(
ctx,
config.Provider.GCP.Project,
config.Provider.GCP.Zone,
config.Provider.GCP.Region,
name,
)
cl, err := c.newTerraformClient(ctx, provider)
if err != nil {
return state.ConstellationState{}, err
}
defer cl.Close()
return c.createGCP(ctx, cl, config, insType, controlPlaneCount, workerCount, ingressRules)
defer cl.RemoveInstaller()
return c.createGCP(ctx, cl, config, name, insType, controlPlaneCount, workerCount)
case cloudprovider.Azure:
cl, err := c.newAzureClient(
config.Provider.Azure.SubscriptionID,
@ -88,7 +73,10 @@ func (c *Creator) Create(ctx context.Context, provider cloudprovider.Provider, c
}
return c.createAzure(ctx, cl, config, insType, controlPlaneCount, workerCount, ingressRules)
case cloudprovider.QEMU:
cl, err := c.newQEMUClient(ctx)
if runtime.GOARCH != "amd64" || runtime.GOOS != "linux" {
return state.ConstellationState{}, fmt.Errorf("creation of a QEMU based Constellation is not supported for %s/%s", runtime.GOOS, runtime.GOARCH)
}
cl, err := c.newTerraformClient(ctx, provider)
if err != nil {
return state.ConstellationState{}, err
}
@ -99,75 +87,29 @@ func (c *Creator) Create(ctx context.Context, provider cloudprovider.Provider, c
}
}
func (c *Creator) createGCP(ctx context.Context, cl gcpclient, config *config.Config, insType string, controlPlaneCount, workerCount int, ingressRules cloudtypes.Firewall,
func (c *Creator) createGCP(ctx context.Context, cl terraformClient, config *config.Config,
name, insType string, controlPlaneCount, workerCount int,
) (stat state.ConstellationState, retErr error) {
defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerGCP{client: cl})
defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerTerraform{client: cl})
if err := cl.CreateVPCs(ctx); err != nil {
return state.ConstellationState{}, err
}
if err := cl.CreateFirewall(ctx, gcpcl.FirewallInput{
Ingress: ingressRules,
Egress: constants.EgressRules,
}); err != nil {
return state.ConstellationState{}, err
}
// additionally create allow-internal rules
internalFirewallInput := gcpcl.FirewallInput{
Ingress: cloudtypes.Firewall{
{
Name: "allow-cluster-internal-tcp",
Protocol: "tcp",
IPRange: gcpcl.SubnetExtCIDR,
},
{
Name: "allow-cluster-internal-udp",
Protocol: "udp",
IPRange: gcpcl.SubnetExtCIDR,
},
{
Name: "allow-cluster-internal-icmp",
Protocol: "icmp",
IPRange: gcpcl.SubnetExtCIDR,
},
{
Name: "allow-node-internal-tcp",
Protocol: "tcp",
IPRange: gcpcl.SubnetCIDR,
},
{
Name: "allow-node-internal-udp",
Protocol: "udp",
IPRange: gcpcl.SubnetCIDR,
},
{
Name: "allow-node-internal-icmp",
Protocol: "icmp",
IPRange: gcpcl.SubnetCIDR,
},
vars := &terraform.GCPVariables{
CommonVariables: terraform.CommonVariables{
Name: name,
CountControlPlanes: controlPlaneCount,
CountWorkers: workerCount,
StateDiskSizeGB: config.StateDiskSizeGB,
},
}
if err := cl.CreateFirewall(ctx, internalFirewallInput); err != nil {
return state.ConstellationState{}, err
Project: config.Provider.GCP.Project,
Region: config.Provider.GCP.Region,
Zone: config.Provider.GCP.Zone,
CredentialsFile: config.Provider.GCP.ServiceAccountKeyPath,
InstanceType: insType,
StateDiskType: config.Provider.GCP.StateDiskType,
ImageID: config.Provider.GCP.Image,
Debug: config.IsDebugCluster(),
}
createInput := gcpcl.CreateInstancesInput{
EnableSerialConsole: config.IsDebugCluster(),
CountControlPlanes: controlPlaneCount,
CountWorkers: workerCount,
ImageID: config.Provider.GCP.Image,
InstanceType: insType,
StateDiskSizeGB: config.StateDiskSizeGB,
StateDiskType: config.Provider.GCP.StateDiskType,
KubeEnv: gcp.KubeEnv,
}
if err := cl.CreateInstances(ctx, createInput); err != nil {
return state.ConstellationState{}, err
}
if err := cl.CreateLoadBalancers(ctx, config.IsDebugCluster()); err != nil {
if err := cl.CreateCluster(ctx, name, vars); err != nil {
return state.ConstellationState{}, err
}
@ -211,25 +153,27 @@ func (c *Creator) createAzure(ctx context.Context, cl azureclient, config *confi
return cl.GetState(), nil
}
func (c *Creator) createQEMU(ctx context.Context, cl qemuclient, name string, config *config.Config, controlPlaneCount, workerCount int,
func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, name string, config *config.Config,
controlPlaneCount, workerCount int,
) (stat state.ConstellationState, retErr error) {
defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerQEMU{client: cl})
defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerTerraform{client: cl})
input := terraform.CreateClusterInput{
CountControlPlanes: controlPlaneCount,
CountWorkers: workerCount,
QEMU: terraform.QEMUInput{
ImagePath: config.Provider.QEMU.Image,
ImageFormat: config.Provider.QEMU.ImageFormat,
CPUCount: config.Provider.QEMU.VCPUs,
MemorySizeMiB: config.Provider.QEMU.Memory,
StateDiskSizeGB: config.StateDiskSizeGB,
IPRangeStart: config.Provider.QEMU.IPRangeStart,
MetadataAPIImage: config.Provider.QEMU.MetadataAPIImage,
vars := &terraform.QEMUVariables{
CommonVariables: terraform.CommonVariables{
Name: name,
CountControlPlanes: controlPlaneCount,
CountWorkers: workerCount,
StateDiskSizeGB: config.StateDiskSizeGB,
},
ImagePath: config.Provider.QEMU.Image,
ImageFormat: config.Provider.QEMU.ImageFormat,
CPUCount: config.Provider.QEMU.VCPUs,
MemorySizeMiB: config.Provider.QEMU.Memory,
IPRangeStart: config.Provider.QEMU.IPRangeStart,
MetadataAPIImage: config.Provider.QEMU.MetadataAPIImage,
}
if err := cl.CreateCluster(ctx, name, input); err != nil {
if err := cl.CreateCluster(ctx, name, vars); err != nil {
return state.ConstellationState{}, err
}

View file

@ -21,29 +21,8 @@ import (
func TestCreator(t *testing.T) {
wantGCPState := state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPProject: "project",
GCPControlPlaneInstances: cloudtypes.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"},
},
GCPWorkerInstances: cloudtypes.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"},
},
GCPWorkerInstanceGroup: "workers-group",
GCPControlPlaneInstanceGroup: "controlplane-group",
GCPWorkerInstanceTemplate: "worker-template",
GCPControlPlaneInstanceTemplate: "controlplane-template",
GCPNetwork: "network",
GCPSubnetwork: "subnetwork",
GCPLoadbalancers: []string{"kube-lb", "boot-lb", "verify-lb"},
GCPFirewalls: []string{
"bootstrapper", "ssh", "nodeport", "kubernetes", "konnectivity", "recovery",
"allow-cluster-internal-tcp", "allow-cluster-internal-udp", "allow-cluster-internal-icmp",
"allow-node-internal-tcp", "allow-node-internal-udp", "allow-node-internal-icmp",
},
CloudProvider: cloudprovider.GCP.String(),
LoadBalancerIP: "192.0.2.1",
}
wantAzureState := state.ConstellationState{
@ -66,8 +45,8 @@ func TestCreator(t *testing.T) {
someErr := errors.New("failed")
testCases := map[string]struct {
gcpclient gcpclient
newGCPClientErr error
tfClient terraformClient
newTfClientErr error
azureclient azureclient
newAzureClientErr error
provider cloudprovider.Provider
@ -77,40 +56,19 @@ func TestCreator(t *testing.T) {
wantRollback bool // Use only together with stubClients.
}{
"gcp": {
gcpclient: &fakeGcpClient{project: "project"},
tfClient: &stubTerraformClient{state: wantGCPState},
provider: cloudprovider.GCP,
config: config.Default(),
wantState: wantGCPState,
},
"gcp newGCPClient error": {
newGCPClientErr: someErr,
provider: cloudprovider.GCP,
config: config.Default(),
wantErr: true,
newTfClientErr: 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,
},
"gcp CreateLoadBalancer error": {
gcpclient: &stubGcpClient{createLoadBalancerErr: someErr},
"gcp create cluster error": {
tfClient: &stubTerraformClient{createClusterErr: someErr},
provider: cloudprovider.GCP,
config: config.Default(),
wantErr: true,
@ -162,8 +120,8 @@ func TestCreator(t *testing.T) {
creator := &Creator{
out: &bytes.Buffer{},
newGCPClient: func(ctx context.Context, project, zone, region, name string) (gcpclient, error) {
return tc.gcpclient, tc.newGCPClientErr
newTerraformClient: func(ctx context.Context, provider cloudprovider.Provider) (terraformClient, error) {
return tc.tfClient, tc.newTfClientErr
},
newAzureClient: func(subscriptionID, tenantID, name, location, resourceGroup string) (azureclient, error) {
return tc.azureclient, tc.newAzureClientErr
@ -176,12 +134,12 @@ func TestCreator(t *testing.T) {
assert.Error(err)
if tc.wantRollback {
switch tc.provider {
case cloudprovider.QEMU:
fallthrough
case cloudprovider.GCP:
cl := tc.gcpclient.(*stubGcpClient)
assert.True(cl.terminateFirewallCalled)
assert.True(cl.terminateInstancesCalled)
assert.True(cl.terminateVPCsCalled)
assert.True(cl.closeCalled)
cl := tc.tfClient.(*stubTerraformClient)
assert.True(cl.destroyClusterCalled)
assert.True(cl.cleanUpWorkspaceCalled)
case cloudprovider.Azure:
cl := tc.azureclient.(*stubAzureClient)
assert.True(cl.terminateResourceGroupResourcesCalled)

View file

@ -34,19 +34,6 @@ func rollbackOnError(ctx context.Context, w io.Writer, onErr *error, roll rollba
fmt.Fprintln(w, "Rollback succeeded.")
}
type rollbackerGCP struct {
client gcpclient
}
func (r *rollbackerGCP) rollback(ctx context.Context) error {
var err error
err = multierr.Append(err, r.client.TerminateLoadBalancers(ctx))
err = multierr.Append(err, r.client.TerminateInstances(ctx))
err = multierr.Append(err, r.client.TerminateFirewall(ctx))
err = multierr.Append(err, r.client.TerminateVPCs(ctx))
return err
}
type rollbackerAzure struct {
client azureclient
}
@ -55,11 +42,11 @@ func (r *rollbackerAzure) rollback(ctx context.Context) error {
return r.client.TerminateResourceGroupResources(ctx)
}
type rollbackerQEMU struct {
client qemuclient
type rollbackerTerraform struct {
client terraformClient
}
func (r *rollbackerQEMU) rollback(ctx context.Context) error {
func (r *rollbackerTerraform) rollback(ctx context.Context) error {
var err error
err = multierr.Append(err, r.client.DestroyCluster(ctx))
err = multierr.Append(err, r.client.CleanUpWorkspace())

View file

@ -11,7 +11,6 @@ import (
"fmt"
azurecl "github.com/edgelesssys/constellation/v2/cli/internal/azure/client"
gcpcl "github.com/edgelesssys/constellation/v2/cli/internal/gcp/client"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/state"
@ -19,23 +18,19 @@ import (
// Terminator deletes cloud provider resources.
type Terminator struct {
newGCPClient func(ctx context.Context) (gcpclient, error)
newAzureClient func(subscriptionID, tenantID string) (azureclient, error)
newQEMUClient func(ctx context.Context) (qemuclient, error)
newTerraformClient func(ctx context.Context) (terraformClient, 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)
newTerraformClient: func(ctx context.Context) (terraformClient, error) {
return terraform.New(ctx, cloudprovider.GCP)
},
newAzureClient: func(subscriptionID, tenantID string) (azureclient, error) {
return azurecl.NewFromDefault(subscriptionID, tenantID)
},
newQEMUClient: func(ctx context.Context) (qemuclient, error) {
return terraform.New(ctx, cloudprovider.QEMU)
},
}
}
@ -43,57 +38,33 @@ func NewTerminator() *Terminator {
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)
case cloudprovider.GCP:
fallthrough
case cloudprovider.QEMU:
cl, err := t.newQEMUClient(ctx)
cl, err := t.newTerraformClient(ctx)
if err != nil {
return err
}
defer cl.RemoveInstaller()
return t.terminateQEMU(ctx, cl)
return t.terminateTerraform(ctx, cl)
default:
return fmt.Errorf("unsupported provider: %s", provider)
}
}
func (t *Terminator) terminateGCP(ctx context.Context, cl gcpclient, state state.ConstellationState) error {
cl.SetState(state)
if err := cl.TerminateLoadBalancers(ctx); 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 nil
}
func (t *Terminator) terminateAzure(ctx context.Context, cl azureclient, state state.ConstellationState) error {
cl.SetState(state)
return cl.TerminateResourceGroupResources(ctx)
}
func (t *Terminator) terminateQEMU(ctx context.Context, cl qemuclient) error {
func (t *Terminator) terminateTerraform(ctx context.Context, cl terraformClient) error {
if err := cl.DestroyCluster(ctx); err != nil {
return err
}

View file

@ -18,25 +18,6 @@ import (
)
func TestTerminator(t *testing.T) {
someGCPState := func() state.ConstellationState {
return state.ConstellationState{
CloudProvider: cloudprovider.GCP.String(),
GCPProject: "project",
GCPWorkerInstances: cloudtypes.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"},
},
GCPControlPlaneInstances: cloudtypes.Instances{
"id-c": {PrivateIP: "192.0.2.1", PublicIP: "192.0.2.1"},
},
GCPWorkerInstanceGroup: "worker-group",
GCPControlPlaneInstanceGroup: "controlplane-group",
GCPWorkerInstanceTemplate: "template",
GCPControlPlaneInstanceTemplate: "template",
GCPNetwork: "network",
GCPFirewalls: []string{"a", "b", "c"},
}
}
someAzureState := func() state.ConstellationState {
return state.ConstellationState{
CloudProvider: cloudprovider.Azure.String(),
@ -53,36 +34,45 @@ func TestTerminator(t *testing.T) {
someErr := errors.New("failed")
testCases := map[string]struct {
gcpclient gcpclient
newGCPClientErr error
tfClient terraformClient
newTfClientErr error
azureclient azureclient
newAzureClientErr error
state state.ConstellationState
wantErr bool
}{
"gcp": {
gcpclient: &stubGcpClient{},
state: someGCPState(),
tfClient: &stubTerraformClient{},
state: state.ConstellationState{CloudProvider: cloudprovider.GCP.String()},
},
"gcp newGCPClient error": {
newGCPClientErr: someErr,
state: someGCPState(),
wantErr: true,
"gcp newTfClientErr": {
newTfClientErr: someErr,
state: state.ConstellationState{CloudProvider: cloudprovider.GCP.String()},
wantErr: true,
},
"gcp terminateInstances error": {
gcpclient: &stubGcpClient{terminateInstancesErr: someErr},
state: someGCPState(),
wantErr: true,
"gcp destroy cluster error": {
tfClient: &stubTerraformClient{destroyClusterErr: someErr},
state: state.ConstellationState{CloudProvider: cloudprovider.GCP.String()},
wantErr: true,
},
"gcp terminateFirewall error": {
gcpclient: &stubGcpClient{terminateFirewallErr: someErr},
state: someGCPState(),
wantErr: true,
"gcp clean up workspace error": {
tfClient: &stubTerraformClient{cleanUpWorkspaceErr: someErr},
state: state.ConstellationState{CloudProvider: cloudprovider.GCP.String()},
wantErr: true,
},
"gcp terminateVPCs error": {
gcpclient: &stubGcpClient{terminateVPCsErr: someErr},
state: someGCPState(),
wantErr: true,
"qemu": {
tfClient: &stubTerraformClient{},
state: state.ConstellationState{CloudProvider: cloudprovider.QEMU.String()},
},
"qemu destroy cluster error": {
tfClient: &stubTerraformClient{destroyClusterErr: someErr},
state: state.ConstellationState{CloudProvider: cloudprovider.QEMU.String()},
wantErr: true,
},
"qemu clean up workspace error": {
tfClient: &stubTerraformClient{cleanUpWorkspaceErr: someErr},
state: state.ConstellationState{CloudProvider: cloudprovider.QEMU.String()},
wantErr: true,
},
"azure": {
azureclient: &stubAzureClient{},
@ -109,8 +99,8 @@ func TestTerminator(t *testing.T) {
assert := assert.New(t)
terminator := &Terminator{
newGCPClient: func(ctx context.Context) (gcpclient, error) {
return tc.gcpclient, tc.newGCPClientErr
newTerraformClient: func(ctx context.Context) (terraformClient, error) {
return tc.tfClient, tc.newTfClientErr
},
newAzureClient: func(subscriptionID, tenantID string) (azureclient, error) {
return tc.azureclient, tc.newAzureClientErr
@ -125,11 +115,11 @@ func TestTerminator(t *testing.T) {
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.closeCalled)
fallthrough
case cloudprovider.QEMU:
cl := tc.tfClient.(*stubTerraformClient)
assert.True(cl.destroyClusterCalled)
assert.True(cl.removeInstallerCalled)
case cloudprovider.Azure:
cl := tc.azureclient.(*stubAzureClient)
assert.True(cl.terminateResourceGroupResourcesCalled)

View file

@ -49,8 +49,7 @@ func TestInitArgumentValidation(t *testing.T) {
func TestInitialize(t *testing.T) {
testGcpState := &state.ConstellationState{
CloudProvider: "GCP",
GCPWorkerInstances: cloudtypes.Instances{"id-0": {}, "id-1": {}},
CloudProvider: "GCP",
}
gcpServiceAccKey := &gcpshared.ServiceAccountKey{
Type: "service_account",

View file

@ -1,35 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package terraform
// CreateClusterInput is user configuration for creating a cluster with Terraform.
type CreateClusterInput struct {
// CountControlPlanes is the number of control-plane nodes to create.
CountControlPlanes int
// CountWorkers is the number of worker nodes to create.
CountWorkers int
// QEMU is the configuration for QEMU clusters.
QEMU QEMUInput
}
// QEMUInput is user configuration for creating a QEMU cluster with Terraform.
type QEMUInput struct {
// CPUCount is the number of CPUs to allocate to each node.
CPUCount int
// MemorySizeMiB is the amount of memory to allocate to each node, in MiB.
MemorySizeMiB int
// StateDiskSizeGB is the size of the state disk to allocate to each node, in GB.
StateDiskSizeGB int
// IPRangeStart is the first IP address in the IP range to allocate to the cluster.
IPRangeStart int
// ImagePath is the path to the image to use for the nodes.
ImagePath string
// ImageFormat is the format of the image from ImagePath.
ImageFormat string
// MetadataAPIImage is the container image to use for the metadata API.
MetadataAPIImage string
}

View file

@ -9,7 +9,6 @@ package terraform
import (
"context"
"errors"
"fmt"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/file"
@ -59,7 +58,7 @@ func New(ctx context.Context, provider cloudprovider.Provider) (*Client, error)
}
// CreateCluster creates a Constellation cluster using Terraform.
func (c *Client) CreateCluster(ctx context.Context, name string, input CreateClusterInput) error {
func (c *Client) CreateCluster(ctx context.Context, name string, vars Variables) error {
if err := prepareWorkspace(c.file, c.provider); err != nil {
return err
}
@ -68,7 +67,7 @@ func (c *Client) CreateCluster(ctx context.Context, name string, input CreateClu
return err
}
if err := writeUserConfig(c.file, c.provider, name, input); err != nil {
if err := c.file.Write(terraformVarsFile, []byte(vars.String())); err != nil {
return err
}
@ -137,35 +136,6 @@ func (c *Client) GetState() state.ConstellationState {
return c.state
}
// writeUserConfig writes the user config file for Terraform.
func writeUserConfig(file file.Handler, provider cloudprovider.Provider, name string, input CreateClusterInput) error {
var userConfig string
switch provider {
case cloudprovider.QEMU:
userConfig = fmt.Sprintf(`
constellation_coreos_image = "%s"
image_format = "%s"
control_plane_count = %d
worker_count = %d
vcpus = %d
memory = %d
state_disk_size = %d
ip_range_start = %d
metadata_api_image = "%s"
name = "%s"
`,
input.QEMU.ImagePath, input.QEMU.ImageFormat,
input.CountControlPlanes, input.CountWorkers,
input.QEMU.CPUCount, input.QEMU.MemorySizeMiB, input.QEMU.StateDiskSizeGB,
input.QEMU.IPRangeStart,
input.QEMU.MetadataAPIImage,
name,
)
}
return file.Write(terraformVarsFile, []byte(userConfig))
}
// GetExecutable returns a Terraform executable either from the local filesystem,
// or downloads the latest version fulfilling the version constraint.
func GetExecutable(ctx context.Context, workingDir string) (terraform *tfexec.Terraform, remove func(), err error) {

View file

@ -0,0 +1,210 @@
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "4.34.0"
}
random = {
source = "hashicorp/random"
version = "3.4.1"
}
}
}
provider "google" {
credentials = file(var.credentials_file)
project = var.project
region = var.region
zone = var.zone
}
locals {
uid = random_id.uid.hex
name = "${var.name}-${local.uid}"
tag = "constellation-${local.uid}"
ports_node_range = "30000-32767"
ports_kubernetes = "6443"
ports_bootstrapper = "9000"
ports_konnectivity = "8132"
ports_verify = "30081"
ports_recovery = "9999"
ports_debugd = "4000"
cidr_vpc_subnet_nodes = "192.168.178.0/24"
cidr_vpc_subnet_pods = "10.10.0.0/16"
kube_env = "AUTOSCALER_ENV_VARS: kube_reserved=cpu=1060m,memory=1019Mi,ephemeral-storage=41Gi;node_labels=;os=linux;os_distribution=cos;evictionHard="
}
resource "random_id" "uid" {
byte_length = 4
}
resource "google_compute_network" "vpc_network" {
name = local.name
description = "Constellation VPC network"
auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "vpc_subnetwork" {
name = local.name
description = "Constellation VPC subnetwork"
network = google_compute_network.vpc_network.id
ip_cidr_range = local.cidr_vpc_subnet_nodes
secondary_ip_range = [
{
range_name = local.name,
ip_cidr_range = local.cidr_vpc_subnet_pods,
}
]
}
resource "google_compute_firewall" "firewall_external" {
name = local.name
description = "Constellation VPC firewall"
network = google_compute_network.vpc_network.id
source_ranges = ["0.0.0.0/0"]
direction = "INGRESS"
allow {
protocol = "tcp"
ports = flatten([
local.ports_node_range,
local.ports_bootstrapper,
local.ports_kubernetes,
local.ports_konnectivity,
local.ports_recovery,
var.debug ? [local.ports_debugd] : [],
])
}
}
resource "google_compute_firewall" "firewall_internal_nodes" {
name = "${local.name}-nodes"
description = "Constellation VPC firewall"
network = google_compute_network.vpc_network.id
source_ranges = [local.cidr_vpc_subnet_nodes]
direction = "INGRESS"
allow { protocol = "tcp" }
allow { protocol = "udp" }
allow { protocol = "icmp" }
}
resource "google_compute_firewall" "firewall_internal_pods" {
name = "${local.name}-pods"
description = "Constellation VPC firewall"
network = google_compute_network.vpc_network.id
source_ranges = [local.cidr_vpc_subnet_pods]
direction = "INGRESS"
allow { protocol = "tcp" }
allow { protocol = "udp" }
allow { protocol = "icmp" }
}
module "instance_group_control_plane" {
source = "./modules/instance_group"
name = local.name
role = "ControlPlane"
uid = local.uid
instance_type = var.instance_type
instance_count = var.control_plane_count
image_id = var.image_id
disk_size = var.state_disk_size
disk_type = var.state_disk_type
network = google_compute_network.vpc_network.id
subnetwork = google_compute_subnetwork.vpc_subnetwork.id
kube_env = local.kube_env
named_ports = flatten([
{ name = "kubernetes", port = local.ports_kubernetes },
{ name = "bootstrapper", port = local.ports_bootstrapper },
{ name = "verify", port = local.ports_verify },
{ name = "konnectivity", port = local.ports_konnectivity },
{ name = "recovery", port = local.ports_recovery },
var.debug ? [{ name = "debugd", port = local.ports_debugd }] : [],
])
}
module "instance_group_worker" {
source = "./modules/instance_group"
name = local.name
role = "Worker"
uid = local.uid
instance_type = var.instance_type
instance_count = var.worker_count
image_id = var.image_id
disk_size = var.state_disk_size
disk_type = var.state_disk_type
network = google_compute_network.vpc_network.id
subnetwork = google_compute_subnetwork.vpc_subnetwork.id
kube_env = local.kube_env
}
resource "google_compute_global_address" "loadbalancer_ip" {
name = local.name
}
module "loadbalancer_kube" {
source = "./modules/loadbalancer"
name = local.name
health_check = "HTTPS"
backend_port_name = "kubernetes"
backend_instance_group = module.instance_group_control_plane.instance_group
ip_address = google_compute_global_address.loadbalancer_ip.self_link
port = local.ports_kubernetes
frontend_labels = {
constellation-uid = local.uid
}
}
module "loadbalancer_boot" {
source = "./modules/loadbalancer"
name = local.name
health_check = "TCP"
backend_port_name = "bootstrapper"
backend_instance_group = module.instance_group_control_plane.instance_group
ip_address = google_compute_global_address.loadbalancer_ip.self_link
port = local.ports_bootstrapper
}
module "loadbalancer_verify" {
source = "./modules/loadbalancer"
name = local.name
health_check = "TCP"
backend_port_name = "verify"
backend_instance_group = module.instance_group_control_plane.instance_group
ip_address = google_compute_global_address.loadbalancer_ip.self_link
port = local.ports_verify
}
module "loadbalancer_konnectivity" {
source = "./modules/loadbalancer"
name = local.name
health_check = "TCP"
backend_port_name = "konnectivity"
backend_instance_group = module.instance_group_control_plane.instance_group
ip_address = google_compute_global_address.loadbalancer_ip.self_link
port = local.ports_konnectivity
}
module "loadbalancer_recovery" {
source = "./modules/loadbalancer"
name = local.name
health_check = "TCP"
backend_port_name = "recovery"
backend_instance_group = module.instance_group_control_plane.instance_group
ip_address = google_compute_global_address.loadbalancer_ip.self_link
port = local.ports_recovery
}
module "loadbalancer_debugd" {
count = var.debug ? 1 : 0 // only deploy debugd in debug mode
source = "./modules/loadbalancer"
name = local.name
health_check = "TCP"
backend_port_name = "debugd"
backend_instance_group = module.instance_group_control_plane.instance_group
ip_address = google_compute_global_address.loadbalancer_ip.self_link
port = local.ports_debugd
}

View file

@ -0,0 +1,97 @@
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "4.34.0"
}
}
}
locals {
role_dashed = var.role == "ControlPlane" ? "control-plane" : "worker"
name = "${var.name}-${local.role_dashed}"
}
resource "google_compute_instance_template" "template" {
name = local.name
machine_type = var.instance_type
tags = ["constellation-${var.uid}"]
confidential_instance_config {
enable_confidential_compute = true
}
disk {
disk_size_gb = 10
source_image = var.image_id
auto_delete = true
boot = true
mode = "READ_WRITE"
}
disk {
disk_size_gb = var.disk_size
disk_type = var.disk_type
auto_delete = true
device_name = "state-disk" // This name is used by disk mapper to find the disk
boot = false
mode = "READ_WRITE"
type = "PERSISTENT"
}
metadata = {
kube-env = var.kube_env
constellation-uid = var.uid
constellation-role = var.role
}
network_interface {
network = var.network
subnetwork = var.subnetwork
access_config {}
alias_ip_range {
ip_cidr_range = "/24"
subnetwork_range_name = var.name
}
}
scheduling {
on_host_maintenance = "TERMINATE"
}
service_account {
scopes = [
"https://www.googleapis.com/auth/compute",
"https://www.googleapis.com/auth/servicecontrol",
"https://www.googleapis.com/auth/service.management.readonly",
"https://www.googleapis.com/auth/devstorage.read_only",
"https://www.googleapis.com/auth/logging.write",
"https://www.googleapis.com/auth/monitoring.write",
"https://www.googleapis.com/auth/trace.append",
]
}
shielded_instance_config {
enable_secure_boot = true
enable_vtpm = true
enable_integrity_monitoring = true
}
}
resource "google_compute_instance_group_manager" "instance_group_manager" {
name = local.name
base_instance_name = local.name
target_size = var.instance_count
version {
instance_template = google_compute_instance_template.template.id
}
dynamic "named_port" {
for_each = toset(var.named_ports)
content {
name = named_port.value.name
port = named_port.value.port
}
}
}

View file

@ -0,0 +1,3 @@
output "instance_group" {
value = google_compute_instance_group_manager.instance_group_manager.instance_group
}

View file

@ -0,0 +1,60 @@
variable "name" {
type = string
description = "Base name of the instance group."
}
variable "role" {
type = string
description = "The role of the instance group. Has to be 'ControlPlane' or 'Worker'."
}
variable "uid" {
type = string
description = "UID of the cluster. This is used for tags."
}
variable "instance_type" {
type = string
description = "Instance type for the nodes."
}
variable "instance_count" {
type = number
description = "Number of instances in the instance group."
}
variable "image_id" {
type = string
description = "Image ID for the nodes."
}
variable "disk_size" {
type = number
description = "Disk size for the nodes, in GB."
}
variable "disk_type" {
type = string
description = "Disk type for the nodes. Has to be 'pd-standard' or 'pd-ssd'."
}
variable "network" {
type = string
description = "Name of the network to use."
}
variable "subnetwork" {
type = string
description = "Name of the subnetwork to use."
}
variable "kube_env" {
type = string
description = "Kubernetes env."
}
variable "named_ports" {
type = list(object({ name = string, port = number }))
default = []
description = "Named ports for the instance group."
}

View file

@ -0,0 +1,62 @@
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "4.34.0"
}
}
}
locals {
name = "${var.name}-${var.backend_port_name}"
}
resource "google_compute_health_check" "health" {
name = local.name
check_interval_sec = 1
timeout_sec = 1
dynamic "tcp_health_check" {
for_each = var.health_check == "TCP" ? [1] : []
content {
port = var.port
}
}
dynamic "https_health_check" {
for_each = var.health_check == "HTTPS" ? [1] : []
content {
host = ""
port = var.port
request_path = "/readyz"
}
}
}
resource "google_compute_backend_service" "backend" {
name = local.name
protocol = "TCP"
load_balancing_scheme = "EXTERNAL"
health_checks = [google_compute_health_check.health.self_link]
port_name = var.backend_port_name
backend {
group = var.backend_instance_group
balancing_mode = "UTILIZATION"
}
}
resource "google_compute_target_tcp_proxy" "proxy" {
name = local.name
backend_service = google_compute_backend_service.backend.self_link
}
resource "google_compute_global_forwarding_rule" "forwarding" {
name = local.name
ip_address = var.ip_address
ip_protocol = "TCP"
load_balancing_scheme = "EXTERNAL"
port_range = var.port
target = google_compute_target_tcp_proxy.proxy.self_link
labels = var.frontend_labels
}

View file

@ -0,0 +1,35 @@
variable "name" {
type = string
description = "Base name of the load balancer."
}
variable "health_check" {
type = string
description = "The type of the health check. 'HTTPS' or 'TCP'."
}
variable "backend_port_name" {
type = string
description = "Name of backend port. The same name should appear in the instance groups referenced by this service."
}
variable "backend_instance_group" {
type = string
description = "The URL of the instance group resource from which the load balancer will direct traffic."
}
variable "ip_address" {
type = string
description = "The IP address that this forwarding rule serves. An address can be specified either by a literal IP address or a reference to an existing Address resource."
}
variable "port" {
type = number
description = "The port on which to listen for incoming traffic."
}
variable "frontend_labels" {
type = map(string)
default = {}
description = "Labels to apply to the forwarding rule."
}

View file

@ -0,0 +1,3 @@
output "ip" {
value = google_compute_global_address.loadbalancer_ip.address
}

View file

@ -0,0 +1,63 @@
variable "name" {
type = string
default = "constell"
description = "Base name of the cluster."
}
variable "control_plane_count" {
type = number
description = "The number of control plane nodes to deploy."
}
variable "worker_count" {
type = number
description = "The number of worker nodes to deploy."
}
variable "state_disk_size" {
type = number
default = 30
description = "The size of the state disk in GB."
}
variable "project" {
type = string
description = "The GCP project to deploy the cluster in."
}
variable "region" {
type = string
description = "The GCP region to deploy the cluster in."
}
variable "zone" {
type = string
description = "The GCP zone to deploy the cluster in."
}
variable "credentials_file" {
type = string
description = "The path to the GCP credentials file."
}
variable "instance_type" {
type = string
description = "The GCP instance type to deploy."
}
variable "state_disk_type" {
type = string
default = "pd-ssd"
description = "The type of the state disk."
}
variable "image_id" {
type = string
description = "The GCP image to use for the cluster nodes."
}
variable "debug" {
type = bool
default = false
description = "Enable debug mode. This opens up a debugd port that can be used to deploy a custom bootstrapper."
}

View file

@ -22,9 +22,9 @@ import (
"go.uber.org/multierr"
)
func TestCreateInstances(t *testing.T) {
someErr := errors.New("error")
getState := func() *tfjson.State {
func TestCreateCluster(t *testing.T) {
someErr := errors.New("failed")
newTestState := func() *tfjson.State {
workingState := tfjson.State{
Values: &tfjson.StateValues{
Outputs: map[string]*tfjson.StateOutput{
@ -34,52 +34,59 @@ func TestCreateInstances(t *testing.T) {
},
},
}
return &workingState
}
qemuVars := &QEMUVariables{
CommonVariables: CommonVariables{
Name: "name",
CountControlPlanes: 1,
CountWorkers: 2,
StateDiskSizeGB: 11,
},
CPUCount: 1,
MemorySizeMiB: 1024,
IPRangeStart: 100,
ImagePath: "path",
ImageFormat: "format",
MetadataAPIImage: "api",
}
testCases := map[string]struct {
provider cloudprovider.Provider
input CreateClusterInput
vars Variables
tf *stubTerraform
fs afero.Fs
wantErr bool
}{
"works": {
provider: cloudprovider.QEMU,
tf: &stubTerraform{
showState: getState(),
},
fs: afero.NewMemMapFs(),
vars: qemuVars,
tf: &stubTerraform{showState: newTestState()},
fs: afero.NewMemMapFs(),
},
"init fails": {
provider: cloudprovider.QEMU,
tf: &stubTerraform{
initErr: someErr,
showState: getState(),
},
fs: afero.NewMemMapFs(),
wantErr: true,
tf: &stubTerraform{initErr: someErr},
fs: afero.NewMemMapFs(),
wantErr: true,
},
"apply fails": {
provider: cloudprovider.QEMU,
tf: &stubTerraform{
applyErr: someErr,
showState: getState(),
},
fs: afero.NewMemMapFs(),
wantErr: true,
vars: qemuVars,
tf: &stubTerraform{applyErr: someErr},
fs: afero.NewMemMapFs(),
wantErr: true,
},
"show fails": {
provider: cloudprovider.QEMU,
tf: &stubTerraform{
showErr: someErr,
},
fs: afero.NewMemMapFs(),
wantErr: true,
vars: qemuVars,
tf: &stubTerraform{showErr: someErr},
fs: afero.NewMemMapFs(),
wantErr: true,
},
"no ip": {
provider: cloudprovider.QEMU,
vars: qemuVars,
tf: &stubTerraform{
showState: &tfjson.State{
Values: &tfjson.StateValues{
@ -90,13 +97,24 @@ func TestCreateInstances(t *testing.T) {
fs: afero.NewMemMapFs(),
wantErr: true,
},
"ip has wrong type": {
provider: cloudprovider.QEMU,
vars: qemuVars,
tf: &stubTerraform{
showState: &tfjson.State{
Values: &tfjson.StateValues{
Outputs: map[string]*tfjson.StateOutput{"ip": {Value: 42}},
},
},
},
fs: afero.NewMemMapFs(),
wantErr: true,
},
"prepare workspace fails": {
provider: cloudprovider.QEMU,
tf: &stubTerraform{
showState: getState(),
},
fs: afero.NewReadOnlyFs(afero.NewMemMapFs()),
wantErr: true,
tf: &stubTerraform{showState: newTestState()},
fs: afero.NewReadOnlyFs(afero.NewMemMapFs()),
wantErr: true,
},
}
@ -110,12 +128,12 @@ func TestCreateInstances(t *testing.T) {
file: file.NewHandler(tc.fs),
}
err := c.CreateCluster(context.Background(), "test", tc.input)
err := c.CreateCluster(context.Background(), "test", tc.vars)
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
})
}

View file

@ -0,0 +1,117 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package terraform
import (
"fmt"
"strings"
)
// Variables is a struct that holds all variables that are passed to Terraform.
type Variables interface {
fmt.Stringer
}
// CommonVariables is user configuration for creating a cluster with Terraform.
type CommonVariables struct {
// Name of the cluster.
Name string
// CountControlPlanes is the number of control-plane nodes to create.
CountControlPlanes int
// CountWorkers is the number of worker nodes to create.
CountWorkers int
// StateDiskSizeGB is the size of the state disk to allocate to each node, in GB.
StateDiskSizeGB int
}
// String returns a string representation of the variables, formatted as Terraform variables.
func (v *CommonVariables) String() string {
b := &strings.Builder{}
writeLinef(b, "name = %q", v.Name)
writeLinef(b, "control_plane_count = %d", v.CountControlPlanes)
writeLinef(b, "worker_count = %d", v.CountWorkers)
writeLinef(b, "state_disk_size = %d", v.StateDiskSizeGB)
return b.String()
}
// GCPVariables is user configuration for creating a cluster with Terraform on GCP.
type GCPVariables struct {
// CommonVariables contains common variables.
CommonVariables
// Project is the ID of the GCP project to use.
Project string
// Region is the GCP region to use.
Region string
// Zone is the GCP zone to use.
Zone string
// CredentialsFile is the path to the GCP credentials file.
CredentialsFile string
// InstanceType is the GCP instance type to use.
InstanceType string
// StateDiskType is the GCP disk type to use for the state disk.
StateDiskType string
// ImageID is the ID of the GCP image to use.
ImageID string
// Debug is true if debug mode is enabled.
Debug bool
}
// String returns a string representation of the variables, formatted as Terraform variables.
func (v *GCPVariables) String() string {
b := &strings.Builder{}
b.WriteString(v.CommonVariables.String())
writeLinef(b, "project = %q", v.Project)
writeLinef(b, "region = %q", v.Region)
writeLinef(b, "zone = %q", v.Zone)
writeLinef(b, "credentials_file = %q", v.CredentialsFile)
writeLinef(b, "instance_type = %q", v.InstanceType)
writeLinef(b, "state_disk_type = %q", v.StateDiskType)
writeLinef(b, "image_id = %q", v.ImageID)
writeLinef(b, "debug = %t", v.Debug)
return b.String()
}
// QEMUVariables is user configuration for creating a QEMU cluster with Terraform.
type QEMUVariables struct {
// CommonVariables contains common variables.
CommonVariables
// CPUCount is the number of CPUs to allocate to each node.
CPUCount int
// MemorySizeMiB is the amount of memory to allocate to each node, in MiB.
MemorySizeMiB int
// IPRangeStart is the first IP address in the IP range to allocate to the cluster.
IPRangeStart int
// ImagePath is the path to the image to use for the nodes.
ImagePath string
// ImageFormat is the format of the image from ImagePath.
ImageFormat string
// MetadataAPIImage is the container image to use for the metadata API.
MetadataAPIImage string
}
// String returns a string representation of the variables, formatted as Terraform variables.
func (v *QEMUVariables) String() string {
b := &strings.Builder{}
b.WriteString(v.CommonVariables.String())
writeLinef(b, "constellation_coreos_image = %q", v.ImagePath)
writeLinef(b, "image_format = %q", v.ImageFormat)
writeLinef(b, "vcpus = %d", v.CPUCount)
writeLinef(b, "memory = %d", v.MemorySizeMiB)
writeLinef(b, "ip_range_start = %d", v.IPRangeStart)
writeLinef(b, "metadata_api_image = %q", v.MetadataAPIImage)
return b.String()
}
func writeLinef(builder *strings.Builder, format string, a ...interface{}) {
builder.WriteString(fmt.Sprintf(format, a...))
builder.WriteByte('\n')
}