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

@ -29,6 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
<!-- For changes in existing functionality. -->
- Autoscaling is now directly managed inside Kubernetes, by the Constellation node operator.
- The `constellation create` on GCP now uses Terraform to create and destroy cloud resources.
- GCP instances are now created without public IPs by default.
### Deprecated
<!-- For soon-to-be removed features. -->

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,3 @@
output "ip" {
value = google_compute_global_address.loadbalancer_ip.address
}

View File

@ -4,6 +4,22 @@ variable "name" {
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."
@ -24,27 +40,11 @@ variable "credentials_file" {
description = "The path to the GCP credentials file."
}
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 "instance_type" {
type = string
description = "The GCP instance type to deploy."
}
variable "state_disk_size" {
type = number
default = 30
description = "The size of the state disk in GB."
}
variable "state_disk_type" {
type = string
default = "pd-ssd"

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')
}

10
go.mod
View File

@ -34,10 +34,8 @@ replace github.com/google/go-attestation => github.com/malt3/go-attestation v0.0
require (
cloud.google.com/go/compute v1.7.0
cloud.google.com/go/iam v0.3.0
cloud.google.com/go/kms v1.4.0
cloud.google.com/go/logging v1.4.2
cloud.google.com/go/resourcemanager v1.2.0
cloud.google.com/go/storage v1.22.1
github.com/Azure/azure-sdk-for-go v66.0.0+incompatible
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.1
@ -106,13 +104,9 @@ require (
k8s.io/utils v0.0.0-20220812165043-ad590609e2e5
)
require (
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/zclconf/go-cty v1.11.0 // indirect
)
require (
cloud.google.com/go v0.102.0 // indirect
cloud.google.com/go/iam v0.3.0 // indirect
code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.5.0 // indirect
@ -198,6 +192,7 @@ require (
github.com/gosuri/uitable v0.0.4 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/icholy/replace v0.5.0
github.com/imdario/mergo v0.3.12 // indirect
@ -257,6 +252,7 @@ require (
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xlab/treeprint v1.1.0 // indirect
github.com/zclconf/go-cty v1.11.0 // indirect
go.opencensus.io v0.23.0 // indirect
go.starlark.net v0.0.0-20220223235035-243c74974e97 // indirect
go.uber.org/atomic v1.9.0 // indirect

2
go.sum
View File

@ -71,8 +71,6 @@ cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/pubsub v1.5.0/go.mod h1:ZEwJccE3z93Z2HWvstpri00jOg7oO4UZDtKhwDwqF0w=
cloud.google.com/go/resourcemanager v1.2.0 h1:Oyt8+J80B51HgIPNk3p1ezTamu1wVj2bj7rBwL5Qd6k=
cloud.google.com/go/resourcemanager v1.2.0/go.mod h1:hFYbG0p7E8vVfQO3yfeaqEQVFO6n9gg9W2czYIdSEy4=
cloud.google.com/go/spanner v1.7.0/go.mod h1:sd3K2gZ9Fd0vMPLXzeCrF6fq4i63Q7aTLW/lBIfBkIk=
cloud.google.com/go/spanner v1.17.0/go.mod h1:+17t2ixFwRG4lWRwE+5kipDR9Ef07Jkmc8z0IbMDKUs=
cloud.google.com/go/spanner v1.18.0/go.mod h1:LvAjUXPeJRGNuGpikMULjhLj/t9cRvdc+fxRoLiugXA=

View File

@ -56,22 +56,10 @@ require (
libvirt.org/go/libvirtxml v1.8007.0
)
require (
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hc-install v0.4.0 // indirect
github.com/hashicorp/terraform-exec v0.17.3 // indirect
github.com/hashicorp/terraform-json v0.14.0 // indirect
github.com/zclconf/go-cty v1.11.0 // indirect
)
require (
cloud.google.com/go v0.102.0 // indirect
cloud.google.com/go/iam v0.3.0 // indirect
cloud.google.com/go/kms v1.4.0 // indirect
cloud.google.com/go/resourcemanager v1.2.0 // indirect
cloud.google.com/go/storage v1.22.1 // indirect
code.cloudfoundry.org/clock v0.0.0-20180518195852-02e53af36e6c // indirect
github.com/Azure/azure-sdk-for-go v66.0.0+incompatible // indirect
@ -155,6 +143,13 @@ require (
github.com/googleapis/gax-go/v2 v2.4.0 // indirect
github.com/googleapis/go-type-adapters v1.0.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hc-install v0.4.0 // indirect
github.com/hashicorp/terraform-exec v0.17.3 // indirect
github.com/hashicorp/terraform-json v0.14.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
@ -184,6 +179,7 @@ require (
github.com/theupdateframework/go-tuf v0.3.2 // indirect
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
github.com/xanzy/ssh-agent v0.3.0 // indirect
github.com/zclconf/go-cty v1.11.0 // indirect
go.opencensus.io v0.23.0 // indirect
go.opentelemetry.io/otel/trace v1.3.0 // indirect
go.uber.org/atomic v1.9.0 // indirect

View File

@ -65,8 +65,6 @@ cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
cloud.google.com/go/pubsub v1.5.0/go.mod h1:ZEwJccE3z93Z2HWvstpri00jOg7oO4UZDtKhwDwqF0w=
cloud.google.com/go/resourcemanager v1.2.0 h1:Oyt8+J80B51HgIPNk3p1ezTamu1wVj2bj7rBwL5Qd6k=
cloud.google.com/go/resourcemanager v1.2.0/go.mod h1:hFYbG0p7E8vVfQO3yfeaqEQVFO6n9gg9W2czYIdSEy4=
cloud.google.com/go/spanner v1.7.0/go.mod h1:sd3K2gZ9Fd0vMPLXzeCrF6fq4i63Q7aTLW/lBIfBkIk=
cloud.google.com/go/spanner v1.17.0/go.mod h1:+17t2ixFwRG4lWRwE+5kipDR9Ef07Jkmc8z0IbMDKUs=
cloud.google.com/go/spanner v1.18.0/go.mod h1:LvAjUXPeJRGNuGpikMULjhLj/t9cRvdc+fxRoLiugXA=