mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-26 07:59:37 -05:00
e350ca0f57
* Implement Azure TDX attestation primitives * Add default measurements and claims for Azure TDX * Enable Constellation on Azure TDX --------- Signed-off-by: Daniel Weiße <dw@edgeless.systems>
1367 lines
33 KiB
Go
1367 lines
33 KiB
Go
/*
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
package terraform
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io/fs"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
|
"github.com/edgelesssys/constellation/v2/internal/encoding"
|
|
"github.com/edgelesssys/constellation/v2/internal/file"
|
|
"github.com/edgelesssys/constellation/v2/internal/role"
|
|
"github.com/hashicorp/terraform-exec/tfexec"
|
|
tfjson "github.com/hashicorp/terraform-json"
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/goleak"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
goleak.VerifyTestMain(m, goleak.IgnoreAnyFunction("github.com/bazelbuild/rules_go/go/tools/bzltestutil.RegisterTimeoutHandler.func1"))
|
|
}
|
|
|
|
func TestPrepareCluster(t *testing.T) {
|
|
qemuVars := &QEMUVariables{
|
|
Name: "name",
|
|
NodeGroups: map[string]QEMUNodeGroup{
|
|
"control-plane": {
|
|
Role: role.ControlPlane.TFString(),
|
|
DiskSize: 30,
|
|
CPUCount: 1,
|
|
MemorySize: 1024,
|
|
},
|
|
},
|
|
Machine: "q35",
|
|
}
|
|
|
|
testCases := map[string]struct {
|
|
pathBase string
|
|
provider cloudprovider.Provider
|
|
vars Variables
|
|
fs afero.Fs
|
|
partiallyExtracted bool
|
|
wantErr bool
|
|
}{
|
|
"qemu": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.QEMU,
|
|
vars: qemuVars,
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: false,
|
|
},
|
|
"no vars": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.QEMU,
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"continue on partially extracted": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.QEMU,
|
|
vars: qemuVars,
|
|
fs: afero.NewMemMapFs(),
|
|
partiallyExtracted: true,
|
|
wantErr: false,
|
|
},
|
|
"prepare workspace fails": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.QEMU,
|
|
vars: qemuVars,
|
|
fs: afero.NewReadOnlyFs(afero.NewMemMapFs()),
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
require := require.New(t)
|
|
|
|
c := &Client{
|
|
tf: &stubTerraform{},
|
|
file: file.NewHandler(tc.fs),
|
|
workingDir: "unittest",
|
|
}
|
|
|
|
path := path.Join(tc.pathBase, strings.ToLower(tc.provider.String()))
|
|
err := c.PrepareWorkspace(path, tc.vars)
|
|
|
|
// Test case: Check if we can continue to create on an incomplete workspace.
|
|
if tc.partiallyExtracted {
|
|
require.NoError(c.file.Remove(filepath.Join(c.workingDir, "main.tf")))
|
|
err = c.PrepareWorkspace(path, tc.vars)
|
|
}
|
|
|
|
if tc.wantErr {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
assert.NoError(err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPrepareIAM(t *testing.T) {
|
|
gcpVars := &GCPIAMVariables{
|
|
Project: "const-1234",
|
|
Region: "europe-west1",
|
|
Zone: "europe-west1-a",
|
|
ServiceAccountID: "const-test-case",
|
|
}
|
|
azureVars := &AzureIAMVariables{
|
|
Location: "westus",
|
|
ResourceGroup: "constell-test-rg",
|
|
}
|
|
awsVars := &AWSIAMVariables{
|
|
Region: "eu-east-2a",
|
|
Prefix: "test",
|
|
}
|
|
testCases := map[string]struct {
|
|
pathBase string
|
|
provider cloudprovider.Provider
|
|
vars Variables
|
|
fs afero.Fs
|
|
partiallyExtracted bool
|
|
wantErr bool
|
|
}{
|
|
"no vars": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"invalid path": {
|
|
pathBase: path.Join("abc", "123"),
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"gcp": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.GCP,
|
|
vars: gcpVars,
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: false,
|
|
},
|
|
"continue on partially extracted": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.GCP,
|
|
vars: gcpVars,
|
|
fs: afero.NewMemMapFs(),
|
|
partiallyExtracted: true,
|
|
wantErr: false,
|
|
},
|
|
"azure": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.Azure,
|
|
vars: azureVars,
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: false,
|
|
},
|
|
"aws": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.AWS,
|
|
vars: awsVars,
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
require := require.New(t)
|
|
|
|
c := &Client{
|
|
tf: &stubTerraform{},
|
|
file: file.NewHandler(tc.fs),
|
|
workingDir: constants.TerraformIAMWorkingDir,
|
|
}
|
|
|
|
path := path.Join(tc.pathBase, strings.ToLower(tc.provider.String()))
|
|
err := c.PrepareWorkspace(path, tc.vars)
|
|
|
|
// Test case: Check if we can continue to create on an incomplete workspace.
|
|
if tc.partiallyExtracted {
|
|
require.NoError(c.file.Remove(filepath.Join(c.workingDir, "main.tf")))
|
|
err = c.PrepareWorkspace(path, tc.vars)
|
|
}
|
|
|
|
if tc.wantErr {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
assert.NoError(err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateCluster(t *testing.T) {
|
|
newQEMUState := func() *tfjson.State {
|
|
workingState := tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{
|
|
"out_of_cluster_endpoint": {
|
|
Value: "192.0.2.100",
|
|
},
|
|
"in_cluster_endpoint": {
|
|
Value: "192.0.2.101",
|
|
},
|
|
"init_secret": {
|
|
Value: "initSecret",
|
|
},
|
|
"uid": {
|
|
Value: "12345abc",
|
|
},
|
|
"api_server_cert_sans": {
|
|
Value: []any{"192.0.2.100"},
|
|
},
|
|
"name": {
|
|
Value: "constell-12345abc",
|
|
},
|
|
"ip_cidr_node": {
|
|
Value: "192.0.2.103/32",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return &workingState
|
|
}
|
|
newAzureState := func() *tfjson.State {
|
|
workingState := tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{
|
|
"out_of_cluster_endpoint": {
|
|
Value: "192.0.2.100",
|
|
},
|
|
"in_cluster_endpoint": {
|
|
Value: "192.0.2.101",
|
|
},
|
|
"init_secret": {
|
|
Value: "initSecret",
|
|
},
|
|
"uid": {
|
|
Value: "12345abc",
|
|
},
|
|
"attestation_url": {
|
|
Value: "https://12345.neu.attest.azure.net",
|
|
},
|
|
"api_server_cert_sans": {
|
|
Value: []any{"192.0.2.100"},
|
|
},
|
|
"user_assigned_identity_client_id": {
|
|
Value: "test_uami_id",
|
|
},
|
|
"resource_group": {
|
|
Value: "test_rg",
|
|
},
|
|
"subscription_id": {
|
|
Value: "test_subscription_id",
|
|
},
|
|
"network_security_group_name": {
|
|
Value: "test_nsg_name",
|
|
},
|
|
"loadbalancer_name": {
|
|
Value: "test_lb_name",
|
|
},
|
|
"name": {
|
|
Value: "constell-12345abc",
|
|
},
|
|
"ip_cidr_node": {
|
|
Value: "192.0.2.103/32",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return &workingState
|
|
}
|
|
qemuVars := &QEMUVariables{
|
|
Name: "name",
|
|
NodeGroups: map[string]QEMUNodeGroup{
|
|
"control-plane": {
|
|
Role: role.ControlPlane.TFString(),
|
|
DiskSize: 11,
|
|
CPUCount: 1,
|
|
MemorySize: 1024,
|
|
},
|
|
},
|
|
Machine: "q35",
|
|
ImagePath: "path",
|
|
ImageFormat: "format",
|
|
MetadataAPIImage: "api",
|
|
}
|
|
|
|
testCases := map[string]struct {
|
|
pathBase string
|
|
provider cloudprovider.Provider
|
|
vars Variables
|
|
tf *stubTerraform
|
|
fs afero.Fs
|
|
// expectedAttestationURL is the expected attestation URL to be returned by
|
|
// the Terraform client. It is declared in the test case because it is
|
|
// provider-specific.
|
|
expectedAttestationURL string
|
|
wantErr bool
|
|
}{
|
|
"works": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.QEMU,
|
|
vars: qemuVars,
|
|
tf: &stubTerraform{showState: newQEMUState()},
|
|
fs: afero.NewMemMapFs(),
|
|
},
|
|
"init fails": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.QEMU,
|
|
vars: qemuVars,
|
|
tf: &stubTerraform{initErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"apply fails": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.QEMU,
|
|
vars: qemuVars,
|
|
tf: &stubTerraform{applyErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"show fails": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.QEMU,
|
|
vars: qemuVars,
|
|
tf: &stubTerraform{showErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"set log fails": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.QEMU,
|
|
vars: qemuVars,
|
|
tf: &stubTerraform{setLogErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"set log path fails": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.QEMU,
|
|
vars: qemuVars,
|
|
tf: &stubTerraform{setLogPathErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"no ip": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.QEMU,
|
|
vars: qemuVars,
|
|
tf: &stubTerraform{
|
|
showState: &tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{},
|
|
},
|
|
},
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"ip has wrong type": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
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,
|
|
},
|
|
"no uid": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.QEMU,
|
|
vars: qemuVars,
|
|
tf: &stubTerraform{
|
|
showState: &tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{},
|
|
},
|
|
},
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"uid has wrong type": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.QEMU,
|
|
vars: qemuVars,
|
|
tf: &stubTerraform{
|
|
showState: &tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{"uid": {Value: 42}},
|
|
},
|
|
},
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"name has wrong type": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.QEMU,
|
|
vars: qemuVars,
|
|
tf: &stubTerraform{
|
|
showState: &tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{"name": {Value: 42}},
|
|
},
|
|
},
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"working attestation url": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.Azure,
|
|
vars: qemuVars, // works for mocking azure vars
|
|
tf: &stubTerraform{showState: newAzureState()},
|
|
fs: afero.NewMemMapFs(),
|
|
expectedAttestationURL: "https://12345.neu.attest.azure.net",
|
|
},
|
|
"no attestation url": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.Azure,
|
|
vars: qemuVars, // works for mocking azure vars
|
|
tf: &stubTerraform{
|
|
showState: &tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{},
|
|
},
|
|
},
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"attestation url has wrong type": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
provider: cloudprovider.Azure,
|
|
vars: qemuVars, // works for mocking azure vars
|
|
tf: &stubTerraform{
|
|
showState: &tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{"attestation_url": {Value: 42}},
|
|
},
|
|
},
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
require := require.New(t)
|
|
|
|
c := &Client{
|
|
tf: tc.tf,
|
|
file: file.NewHandler(tc.fs),
|
|
workingDir: "unittest",
|
|
}
|
|
|
|
path := path.Join(tc.pathBase, strings.ToLower(tc.provider.String()))
|
|
require.NoError(c.PrepareWorkspace(path, tc.vars))
|
|
infraState, err := c.ApplyCluster(context.Background(), tc.provider, LogLevelDebug)
|
|
|
|
if tc.wantErr {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
assert.NoError(err)
|
|
assert.Equal("192.0.2.100", infraState.ClusterEndpoint)
|
|
assert.Equal(encoding.HexBytes("initSecret"), infraState.InitSecret)
|
|
assert.Equal("12345abc", infraState.UID)
|
|
assert.Equal("192.0.2.101", infraState.InClusterEndpoint)
|
|
assert.Equal("192.0.2.103/32", infraState.IPCidrNode)
|
|
if tc.provider == cloudprovider.Azure {
|
|
assert.Equal(tc.expectedAttestationURL, infraState.Azure.AttestationURL)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCreateIAM(t *testing.T) {
|
|
newTestState := func() *tfjson.State {
|
|
workingState := tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{
|
|
"service_account_key": {
|
|
Value: "12345678_abcdefg",
|
|
},
|
|
"subscription_id": {
|
|
Value: "test_subscription_id",
|
|
},
|
|
"tenant_id": {
|
|
Value: "test_tenant_id",
|
|
},
|
|
"application_id": {
|
|
Value: "test_application_id",
|
|
},
|
|
"uami_id": {
|
|
Value: "test_uami_id",
|
|
},
|
|
"application_client_secret_value": {
|
|
Value: "test_application_client_secret_value",
|
|
},
|
|
"iam_instance_profile_name_control_plane": {
|
|
Value: "test_iam_instance_profile_name_control_plane",
|
|
},
|
|
"iam_instance_profile_name_worker_nodes": {
|
|
Value: "test_iam_instance_profile_name_worker_nodes",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
return &workingState
|
|
}
|
|
gcpVars := &GCPIAMVariables{
|
|
Project: "const-1234",
|
|
Region: "europe-west1",
|
|
Zone: "europe-west1-a",
|
|
ServiceAccountID: "const-test-case",
|
|
}
|
|
azureVars := &AzureIAMVariables{
|
|
Location: "westus",
|
|
ResourceGroup: "constell-test-rg",
|
|
}
|
|
awsVars := &AWSIAMVariables{
|
|
Region: "eu-east-2a",
|
|
Prefix: "test",
|
|
}
|
|
|
|
testCases := map[string]struct {
|
|
pathBase string
|
|
provider cloudprovider.Provider
|
|
vars Variables
|
|
tf *stubTerraform
|
|
fs afero.Fs
|
|
wantErr bool
|
|
want IAMOutput
|
|
}{
|
|
"set log fails": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.GCP,
|
|
vars: gcpVars,
|
|
tf: &stubTerraform{setLogErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"set log path fails": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.GCP,
|
|
vars: gcpVars,
|
|
tf: &stubTerraform{setLogPathErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"gcp works": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.GCP,
|
|
vars: gcpVars,
|
|
tf: &stubTerraform{showState: newTestState()},
|
|
fs: afero.NewMemMapFs(),
|
|
want: IAMOutput{GCP: GCPIAMOutput{SaKey: "12345678_abcdefg"}},
|
|
},
|
|
"gcp init fails": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.GCP,
|
|
vars: gcpVars,
|
|
tf: &stubTerraform{initErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"gcp apply fails": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.GCP,
|
|
vars: gcpVars,
|
|
tf: &stubTerraform{applyErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"gcp show fails": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.GCP,
|
|
vars: gcpVars,
|
|
tf: &stubTerraform{showErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"gcp no service_account_key": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.GCP,
|
|
vars: gcpVars,
|
|
tf: &stubTerraform{
|
|
showState: &tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{},
|
|
},
|
|
},
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"gcp service_account_key has wrong type": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.GCP,
|
|
vars: gcpVars,
|
|
tf: &stubTerraform{
|
|
showState: &tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{"service_account_key": {Value: 42}},
|
|
},
|
|
},
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"azure works": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.Azure,
|
|
vars: azureVars,
|
|
tf: &stubTerraform{showState: newTestState()},
|
|
fs: afero.NewMemMapFs(),
|
|
want: IAMOutput{Azure: AzureIAMOutput{
|
|
SubscriptionID: "test_subscription_id",
|
|
TenantID: "test_tenant_id",
|
|
UAMIID: "test_uami_id",
|
|
}},
|
|
},
|
|
"azure init fails": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.Azure,
|
|
vars: azureVars,
|
|
tf: &stubTerraform{initErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"azure apply fails": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.Azure,
|
|
vars: azureVars,
|
|
tf: &stubTerraform{applyErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"azure show fails": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.Azure,
|
|
vars: azureVars,
|
|
tf: &stubTerraform{showErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"azure no subscription_id": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.Azure,
|
|
vars: azureVars,
|
|
tf: &stubTerraform{
|
|
showState: &tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{},
|
|
},
|
|
},
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"azure subscription_id has wrong type": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.Azure,
|
|
vars: azureVars,
|
|
tf: &stubTerraform{
|
|
showState: &tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{"subscription_id": {Value: 42}},
|
|
},
|
|
},
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"aws works": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.AWS,
|
|
vars: awsVars,
|
|
tf: &stubTerraform{showState: newTestState()},
|
|
fs: afero.NewMemMapFs(),
|
|
want: IAMOutput{AWS: AWSIAMOutput{
|
|
ControlPlaneInstanceProfile: "test_iam_instance_profile_name_control_plane",
|
|
WorkerNodeInstanceProfile: "test_iam_instance_profile_name_worker_nodes",
|
|
}},
|
|
},
|
|
"aws init fails": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.AWS,
|
|
vars: awsVars,
|
|
tf: &stubTerraform{initErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"aws apply fails": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.AWS,
|
|
vars: awsVars,
|
|
tf: &stubTerraform{applyErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"aws show fails": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.AWS,
|
|
vars: awsVars,
|
|
tf: &stubTerraform{showErr: assert.AnError},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"aws no iam_instance_profile_name_control_plane": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.AWS,
|
|
vars: awsVars,
|
|
tf: &stubTerraform{
|
|
showState: &tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{},
|
|
},
|
|
},
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"azure iam_instance_profile_name_control_plane has wrong type": {
|
|
pathBase: path.Join(constants.TerraformEmbeddedDir, "iam"),
|
|
provider: cloudprovider.AWS,
|
|
vars: awsVars,
|
|
tf: &stubTerraform{
|
|
showState: &tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{"iam_instance_profile_name_control_plane": {Value: 42}},
|
|
},
|
|
},
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
require := require.New(t)
|
|
|
|
c := &Client{
|
|
tf: tc.tf,
|
|
file: file.NewHandler(tc.fs),
|
|
workingDir: constants.TerraformIAMWorkingDir,
|
|
}
|
|
|
|
path := path.Join(tc.pathBase, strings.ToLower(tc.provider.String()))
|
|
require.NoError(c.PrepareWorkspace(path, tc.vars))
|
|
IAMoutput, err := c.ApplyIAM(context.Background(), tc.provider, LogLevelDebug)
|
|
|
|
if tc.wantErr {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
assert.NoError(err)
|
|
assert.Equal(tc.want, IAMoutput)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDestroyInstances(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
tf *stubTerraform
|
|
wantErr bool
|
|
}{
|
|
"works": {
|
|
tf: &stubTerraform{},
|
|
},
|
|
"destroy fails": {
|
|
tf: &stubTerraform{destroyErr: assert.AnError},
|
|
wantErr: true,
|
|
},
|
|
"setLog fails": {
|
|
tf: &stubTerraform{setLogErr: assert.AnError},
|
|
wantErr: true,
|
|
},
|
|
"setLogPath fails": {
|
|
tf: &stubTerraform{setLogPathErr: assert.AnError},
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
c := &Client{
|
|
tf: tc.tf,
|
|
}
|
|
|
|
err := c.Destroy(context.Background(), LogLevelDebug)
|
|
if tc.wantErr {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
|
|
assert.NoError(err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCleanupWorkspace(t *testing.T) {
|
|
someContent := []byte("some content")
|
|
|
|
testCases := map[string]struct {
|
|
provider cloudprovider.Provider
|
|
prepareFS func(file.Handler) error
|
|
wantErr bool
|
|
}{
|
|
"files are cleaned up": {
|
|
provider: cloudprovider.QEMU,
|
|
prepareFS: func(f file.Handler) error {
|
|
if err := f.Write("terraform.tfvars", someContent); err != nil {
|
|
return err
|
|
}
|
|
if err := f.Write("terraform.tfstate", someContent); err != nil {
|
|
return err
|
|
}
|
|
return f.Write("terraform.tfstate.backup", someContent)
|
|
},
|
|
},
|
|
"no error if files do not exist": {
|
|
provider: cloudprovider.QEMU,
|
|
prepareFS: func(f file.Handler) error { return nil },
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
require := require.New(t)
|
|
|
|
file := file.NewHandler(afero.NewMemMapFs())
|
|
require.NoError(tc.prepareFS(file))
|
|
|
|
c := &Client{
|
|
file: file,
|
|
tf: &stubTerraform{},
|
|
workingDir: "unittest",
|
|
}
|
|
|
|
err := c.CleanUpWorkspace()
|
|
if tc.wantErr {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
assert.NoError(err)
|
|
_, err = file.Stat(filepath.Join(c.workingDir, "terraform.tfvars"))
|
|
assert.ErrorIs(err, fs.ErrNotExist)
|
|
_, err = file.Stat(filepath.Join(c.workingDir, "terraform.tfstate"))
|
|
assert.ErrorIs(err, fs.ErrNotExist)
|
|
_, err = file.Stat(filepath.Join(c.workingDir, "terraform.tfstate.backup"))
|
|
assert.ErrorIs(err, fs.ErrNotExist)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseLogLevel(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
level string
|
|
want LogLevel
|
|
wantErr bool
|
|
}{
|
|
"json": {
|
|
level: "json",
|
|
want: LogLevelJSON,
|
|
},
|
|
"trace": {
|
|
level: "trace",
|
|
want: LogLevelTrace,
|
|
},
|
|
"debug": {
|
|
level: "debug",
|
|
want: LogLevelDebug,
|
|
},
|
|
"info": {
|
|
level: "info",
|
|
want: LogLevelInfo,
|
|
},
|
|
"warn": {
|
|
level: "warn",
|
|
want: LogLevelWarn,
|
|
},
|
|
"error": {
|
|
level: "error",
|
|
want: LogLevelError,
|
|
},
|
|
"none": {
|
|
level: "none",
|
|
want: LogLevelNone,
|
|
},
|
|
"unknown": {
|
|
level: "unknown",
|
|
wantErr: true,
|
|
},
|
|
"empty": {
|
|
level: "",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
level, err := ParseLogLevel(tc.level)
|
|
if tc.wantErr {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
assert.NoError(err)
|
|
assert.Equal(tc.want, level)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLogLevelString(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
level LogLevel
|
|
want string
|
|
}{
|
|
"json": {
|
|
level: LogLevelJSON,
|
|
want: "JSON",
|
|
},
|
|
"trace": {
|
|
level: LogLevelTrace,
|
|
want: "TRACE",
|
|
},
|
|
"debug": {
|
|
level: LogLevelDebug,
|
|
want: "DEBUG",
|
|
},
|
|
"info": {
|
|
level: LogLevelInfo,
|
|
want: "INFO",
|
|
},
|
|
"warn": {
|
|
level: LogLevelWarn,
|
|
want: "WARN",
|
|
},
|
|
"error": {
|
|
level: LogLevelError,
|
|
want: "ERROR",
|
|
},
|
|
"none": {
|
|
level: LogLevelNone,
|
|
want: "",
|
|
},
|
|
"invalid int": {
|
|
level: LogLevel(-1),
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
assert.Equal(tc.want, tc.level.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPlan(t *testing.T) {
|
|
someError := errors.New("some error")
|
|
|
|
testCases := map[string]struct {
|
|
pathBase string
|
|
tf *stubTerraform
|
|
fs afero.Fs
|
|
wantErr bool
|
|
}{
|
|
"plan succeeds": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
tf: &stubTerraform{},
|
|
fs: afero.NewMemMapFs(),
|
|
},
|
|
"set log path fails": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
tf: &stubTerraform{
|
|
setLogPathErr: someError,
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"set log fails": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
tf: &stubTerraform{
|
|
setLogErr: someError,
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"plan fails": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
tf: &stubTerraform{
|
|
planJSONErr: someError,
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"init fails": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
tf: &stubTerraform{
|
|
initErr: someError,
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
c := &Client{
|
|
file: file.NewHandler(tc.fs),
|
|
tf: tc.tf,
|
|
workingDir: tc.pathBase,
|
|
}
|
|
|
|
_, err := c.Plan(context.Background(), LogLevelDebug)
|
|
if tc.wantErr {
|
|
require.Error(err)
|
|
} else {
|
|
require.NoError(err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestShowPlan(t *testing.T) {
|
|
someError := errors.New("some error")
|
|
testCases := map[string]struct {
|
|
pathBase string
|
|
tf *stubTerraform
|
|
fs afero.Fs
|
|
wantErr bool
|
|
}{
|
|
"show plan succeeds": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
tf: &stubTerraform{},
|
|
fs: afero.NewMemMapFs(),
|
|
},
|
|
"set log path fails": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
tf: &stubTerraform{
|
|
setLogPathErr: someError,
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"set log fails": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
tf: &stubTerraform{
|
|
setLogErr: someError,
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
"show plan file fails": {
|
|
pathBase: constants.TerraformEmbeddedDir,
|
|
tf: &stubTerraform{
|
|
showPlanFileErr: someError,
|
|
},
|
|
fs: afero.NewMemMapFs(),
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
c := &Client{
|
|
file: file.NewHandler(tc.fs),
|
|
tf: tc.tf,
|
|
workingDir: tc.pathBase,
|
|
}
|
|
|
|
err := c.ShowPlan(context.Background(), LogLevelDebug, bytes.NewBuffer(nil))
|
|
if tc.wantErr {
|
|
require.Error(err)
|
|
} else {
|
|
require.NoError(err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestShowIAM(t *testing.T) {
|
|
testCases := map[string]struct {
|
|
tf *stubTerraform
|
|
csp cloudprovider.Provider
|
|
wantErr bool
|
|
}{
|
|
"GCP success": {
|
|
tf: &stubTerraform{
|
|
showState: getTfjsonState(map[string]any{
|
|
"service_account_key": "key",
|
|
}),
|
|
},
|
|
csp: cloudprovider.GCP,
|
|
},
|
|
"GCP wrong data type": {
|
|
tf: &stubTerraform{
|
|
showState: getTfjsonState(map[string]any{
|
|
"service_account_key": map[string]any{},
|
|
}),
|
|
},
|
|
csp: cloudprovider.GCP,
|
|
wantErr: true,
|
|
},
|
|
"GCP missing key": {
|
|
tf: &stubTerraform{
|
|
showState: getTfjsonState(map[string]any{}),
|
|
},
|
|
csp: cloudprovider.GCP,
|
|
wantErr: true,
|
|
},
|
|
"Azure success": {
|
|
tf: &stubTerraform{
|
|
showState: getTfjsonState(map[string]any{
|
|
"subscription_id": "sub",
|
|
"tenant_id": "tenant",
|
|
"uami_id": "uami",
|
|
}),
|
|
},
|
|
csp: cloudprovider.Azure,
|
|
},
|
|
"Azure wrong data type subscription_id": {
|
|
tf: &stubTerraform{
|
|
showState: getTfjsonState(map[string]any{
|
|
"subscription_id": map[string]any{},
|
|
"tenant_id": "tenant",
|
|
"uami_id": "uami",
|
|
}),
|
|
},
|
|
csp: cloudprovider.Azure,
|
|
wantErr: true,
|
|
},
|
|
"Azure wrong data type tenant_id": {
|
|
tf: &stubTerraform{
|
|
showState: getTfjsonState(map[string]any{
|
|
"subscription_id": "sub",
|
|
"tenant_id": map[string]any{},
|
|
"uami_id": "uami",
|
|
}),
|
|
},
|
|
csp: cloudprovider.Azure,
|
|
wantErr: true,
|
|
},
|
|
"Azure wrong data type uami_id": {
|
|
tf: &stubTerraform{
|
|
showState: getTfjsonState(map[string]any{
|
|
"subscription_id": "sub",
|
|
"tenant_id": "tenant",
|
|
"uami_id": map[string]any{},
|
|
}),
|
|
},
|
|
csp: cloudprovider.Azure,
|
|
wantErr: true,
|
|
},
|
|
"Azure missing uami_id": {
|
|
tf: &stubTerraform{
|
|
showState: getTfjsonState(map[string]any{
|
|
"subscription_id": "sub",
|
|
"tenant_id": "tenant",
|
|
}),
|
|
},
|
|
csp: cloudprovider.Azure,
|
|
wantErr: true,
|
|
},
|
|
"Azure missing tenant_id": {
|
|
tf: &stubTerraform{
|
|
showState: getTfjsonState(map[string]any{
|
|
"subscription_id": "sub",
|
|
"uami_id": "uami",
|
|
}),
|
|
},
|
|
csp: cloudprovider.Azure,
|
|
wantErr: true,
|
|
},
|
|
"Azure missing subscription_id": {
|
|
tf: &stubTerraform{
|
|
showState: getTfjsonState(map[string]any{
|
|
"tenant_id": "tenant",
|
|
"uami_id": "uami",
|
|
}),
|
|
},
|
|
csp: cloudprovider.Azure,
|
|
wantErr: true,
|
|
},
|
|
"AWS success": {
|
|
tf: &stubTerraform{
|
|
showState: getTfjsonState(map[string]any{
|
|
"iam_instance_profile_name_control_plane": "profile",
|
|
"iam_instance_profile_name_worker_nodes": "profile",
|
|
}),
|
|
},
|
|
csp: cloudprovider.AWS,
|
|
},
|
|
"AWS wrong data type iam_instance_profile_name_control_plane": {
|
|
tf: &stubTerraform{
|
|
showState: getTfjsonState(map[string]any{
|
|
"iam_instance_profile_name_control_plane": map[string]any{},
|
|
"iam_instance_profile_name_worker_nodes": "profile",
|
|
}),
|
|
},
|
|
csp: cloudprovider.AWS,
|
|
wantErr: true,
|
|
},
|
|
"AWS wrong data type iam_instance_profile_name_worker_nodes": {
|
|
tf: &stubTerraform{
|
|
showState: getTfjsonState(map[string]any{
|
|
"iam_instance_profile_name_control_plane": "profile",
|
|
"iam_instance_profile_name_worker_nodes": map[string]any{},
|
|
}),
|
|
},
|
|
csp: cloudprovider.AWS,
|
|
wantErr: true,
|
|
},
|
|
"AWS missing iam_instance_profile_name_control_plane": {
|
|
tf: &stubTerraform{
|
|
showState: getTfjsonState(map[string]any{
|
|
"iam_instance_profile_name_worker_nodes": "profile",
|
|
}),
|
|
},
|
|
csp: cloudprovider.AWS,
|
|
wantErr: true,
|
|
},
|
|
"AWS missing iam_instance_profile_name_worker_nodes": {
|
|
tf: &stubTerraform{
|
|
showState: getTfjsonState(map[string]any{
|
|
"iam_instance_profile_name_control_plane": "profile",
|
|
}),
|
|
},
|
|
csp: cloudprovider.AWS,
|
|
wantErr: true,
|
|
},
|
|
"Show fails": {
|
|
tf: &stubTerraform{
|
|
showErr: assert.AnError,
|
|
},
|
|
csp: cloudprovider.AWS,
|
|
wantErr: true,
|
|
},
|
|
"Show returns state with nil Value": {
|
|
tf: &stubTerraform{
|
|
showState: &tfjson.State{},
|
|
},
|
|
csp: cloudprovider.AWS,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range testCases {
|
|
t.Run(name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
c := &Client{
|
|
tf: tc.tf,
|
|
}
|
|
|
|
_, err := c.ShowIAM(context.Background(), tc.csp)
|
|
if tc.wantErr {
|
|
assert.Error(err)
|
|
return
|
|
}
|
|
assert.NoError(err)
|
|
})
|
|
}
|
|
}
|
|
|
|
type stubTerraform struct {
|
|
applyErr error
|
|
destroyErr error
|
|
initErr error
|
|
showErr error
|
|
setLogErr error
|
|
setLogPathErr error
|
|
planJSONErr error
|
|
showPlanFileErr error
|
|
stateMvErr error
|
|
showState *tfjson.State
|
|
}
|
|
|
|
func (s *stubTerraform) Apply(context.Context, ...tfexec.ApplyOption) error {
|
|
return s.applyErr
|
|
}
|
|
|
|
func (s *stubTerraform) Destroy(context.Context, ...tfexec.DestroyOption) error {
|
|
return s.destroyErr
|
|
}
|
|
|
|
func (s *stubTerraform) Init(context.Context, ...tfexec.InitOption) error {
|
|
return s.initErr
|
|
}
|
|
|
|
func (s *stubTerraform) Show(context.Context, ...tfexec.ShowOption) (*tfjson.State, error) {
|
|
return s.showState, s.showErr
|
|
}
|
|
|
|
func (s *stubTerraform) Plan(context.Context, ...tfexec.PlanOption) (bool, error) {
|
|
return false, s.planJSONErr
|
|
}
|
|
|
|
func (s *stubTerraform) ShowPlanFileRaw(context.Context, string, ...tfexec.ShowOption) (string, error) {
|
|
return "", s.showPlanFileErr
|
|
}
|
|
|
|
func (s *stubTerraform) SetLog(_ string) error {
|
|
return s.setLogErr
|
|
}
|
|
|
|
func (s *stubTerraform) SetLogPath(_ string) error {
|
|
return s.setLogPathErr
|
|
}
|
|
|
|
func (s *stubTerraform) StateMv(_ context.Context, _, _ string, _ ...tfexec.StateMvCmdOption) error {
|
|
return s.stateMvErr
|
|
}
|
|
|
|
func getTfjsonState(values map[string]any) *tfjson.State {
|
|
state := tfjson.State{
|
|
Values: &tfjson.StateValues{
|
|
Outputs: map[string]*tfjson.StateOutput{},
|
|
},
|
|
}
|
|
for k, v := range values {
|
|
state.Values.Outputs[k] = &tfjson.StateOutput{Value: v}
|
|
}
|
|
return &state
|
|
}
|