AB#2579 Add constellation iam create command (#624)

This commit is contained in:
Moritz Sanft 2022-12-07 11:48:54 +01:00 committed by GitHub
parent be01cf7129
commit 286803fb97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 2029 additions and 108 deletions

View File

@ -52,6 +52,7 @@ func NewRootCmd() *cobra.Command {
rootCmd.AddCommand(cmd.NewRecoverCmd()) rootCmd.AddCommand(cmd.NewRecoverCmd())
rootCmd.AddCommand(cmd.NewTerminateCmd()) rootCmd.AddCommand(cmd.NewTerminateCmd())
rootCmd.AddCommand(cmd.NewVersionCmd()) rootCmd.AddCommand(cmd.NewVersionCmd())
rootCmd.AddCommand(cmd.NewIAMCmd())
return rootCmd return rootCmd
} }

View File

@ -16,8 +16,9 @@ import (
) )
type terraformClient interface { type terraformClient interface {
PrepareWorkspace(provider cloudprovider.Provider, input terraform.Variables) error PrepareWorkspace(path string, input terraform.Variables) error
CreateCluster(ctx context.Context) (string, string, error) CreateCluster(ctx context.Context) (string, string, error)
CreateIAMConfig(ctx context.Context, provider cloudprovider.Provider) (terraform.IAMOutput, error)
DestroyCluster(ctx context.Context) error DestroyCluster(ctx context.Context) error
CleanUpWorkspace() error CleanUpWorkspace() error
RemoveInstaller() RemoveInstaller()

View File

@ -28,6 +28,7 @@ func TestMain(m *testing.M) {
type stubTerraformClient struct { type stubTerraformClient struct {
ip string ip string
initSecret string initSecret string
iamOutput terraform.IAMOutput
cleanUpWorkspaceCalled bool cleanUpWorkspaceCalled bool
removeInstallerCalled bool removeInstallerCalled bool
destroyClusterCalled bool destroyClusterCalled bool
@ -35,13 +36,18 @@ type stubTerraformClient struct {
destroyClusterErr error destroyClusterErr error
prepareWorkspaceErr error prepareWorkspaceErr error
cleanUpWorkspaceErr error cleanUpWorkspaceErr error
iamOutputErr error
} }
func (c *stubTerraformClient) CreateCluster(ctx context.Context) (string, string, error) { func (c *stubTerraformClient) CreateCluster(ctx context.Context) (string, string, error) {
return c.ip, c.initSecret, c.createClusterErr return c.ip, c.initSecret, c.createClusterErr
} }
func (c *stubTerraformClient) PrepareWorkspace(provider cloudprovider.Provider, input terraform.Variables) error { func (c *stubTerraformClient) CreateIAMConfig(ctx context.Context, provider cloudprovider.Provider) (terraform.IAMOutput, error) {
return c.iamOutput, c.iamOutputErr
}
func (c *stubTerraformClient) PrepareWorkspace(path string, input terraform.Variables) error {
return c.prepareWorkspaceErr return c.prepareWorkspaceErr
} }

View File

@ -12,6 +12,7 @@ import (
"io" "io"
"net/url" "net/url"
"os" "os"
"path"
"regexp" "regexp"
"runtime" "runtime"
"strings" "strings"
@ -100,7 +101,7 @@ func (c *Creator) Create(ctx context.Context, provider cloudprovider.Provider, c
func (c *Creator) createAWS(ctx context.Context, cl terraformClient, config *config.Config, func (c *Creator) createAWS(ctx context.Context, cl terraformClient, config *config.Config,
name, insType string, controlPlaneCount, workerCount int, image string, name, insType string, controlPlaneCount, workerCount int, image string,
) (idFile clusterid.File, retErr error) { ) (idFile clusterid.File, retErr error) {
vars := terraform.AWSVariables{ vars := terraform.AWSClusterVariables{
CommonVariables: terraform.CommonVariables{ CommonVariables: terraform.CommonVariables{
Name: name, Name: name,
CountControlPlanes: controlPlaneCount, CountControlPlanes: controlPlaneCount,
@ -117,7 +118,7 @@ func (c *Creator) createAWS(ctx context.Context, cl terraformClient, config *con
Debug: config.IsDebugCluster(), Debug: config.IsDebugCluster(),
} }
if err := cl.PrepareWorkspace(cloudprovider.AWS, &vars); err != nil { if err := cl.PrepareWorkspace(path.Join("terraform", strings.ToLower(cloudprovider.AWS.String())), &vars); err != nil {
return clusterid.File{}, err return clusterid.File{}, err
} }
@ -137,7 +138,7 @@ func (c *Creator) createAWS(ctx context.Context, cl terraformClient, config *con
func (c *Creator) createGCP(ctx context.Context, cl terraformClient, config *config.Config, func (c *Creator) createGCP(ctx context.Context, cl terraformClient, config *config.Config,
name, insType string, controlPlaneCount, workerCount int, image string, name, insType string, controlPlaneCount, workerCount int, image string,
) (idFile clusterid.File, retErr error) { ) (idFile clusterid.File, retErr error) {
vars := terraform.GCPVariables{ vars := terraform.GCPClusterVariables{
CommonVariables: terraform.CommonVariables{ CommonVariables: terraform.CommonVariables{
Name: name, Name: name,
CountControlPlanes: controlPlaneCount, CountControlPlanes: controlPlaneCount,
@ -154,7 +155,7 @@ func (c *Creator) createGCP(ctx context.Context, cl terraformClient, config *con
Debug: config.IsDebugCluster(), Debug: config.IsDebugCluster(),
} }
if err := cl.PrepareWorkspace(cloudprovider.GCP, &vars); err != nil { if err := cl.PrepareWorkspace(path.Join("terraform", strings.ToLower(cloudprovider.GCP.String())), &vars); err != nil {
return clusterid.File{}, err return clusterid.File{}, err
} }
@ -174,7 +175,7 @@ func (c *Creator) createGCP(ctx context.Context, cl terraformClient, config *con
func (c *Creator) createAzure(ctx context.Context, cl terraformClient, config *config.Config, func (c *Creator) createAzure(ctx context.Context, cl terraformClient, config *config.Config,
name, insType string, controlPlaneCount, workerCount int, image string, name, insType string, controlPlaneCount, workerCount int, image string,
) (idFile clusterid.File, retErr error) { ) (idFile clusterid.File, retErr error) {
vars := terraform.AzureVariables{ vars := terraform.AzureClusterVariables{
CommonVariables: terraform.CommonVariables{ CommonVariables: terraform.CommonVariables{
Name: name, Name: name,
CountControlPlanes: controlPlaneCount, CountControlPlanes: controlPlaneCount,
@ -194,7 +195,7 @@ func (c *Creator) createAzure(ctx context.Context, cl terraformClient, config *c
vars = normalizeAzureURIs(vars) vars = normalizeAzureURIs(vars)
if err := cl.PrepareWorkspace(cloudprovider.Azure, &vars); err != nil { if err := cl.PrepareWorkspace(path.Join("terraform", strings.ToLower(cloudprovider.Azure.String())), &vars); err != nil {
return clusterid.File{}, err return clusterid.File{}, err
} }
@ -224,7 +225,7 @@ var (
caseInsensitiveVersionsRegExp = regexp.MustCompile(`(?i)\/versions\/`) caseInsensitiveVersionsRegExp = regexp.MustCompile(`(?i)\/versions\/`)
) )
func normalizeAzureURIs(vars terraform.AzureVariables) terraform.AzureVariables { func normalizeAzureURIs(vars terraform.AzureClusterVariables) terraform.AzureClusterVariables {
vars.UserAssignedIdentity = caseInsensitiveSubscriptionsRegexp.ReplaceAllString(vars.UserAssignedIdentity, "/subscriptions/") vars.UserAssignedIdentity = caseInsensitiveSubscriptionsRegexp.ReplaceAllString(vars.UserAssignedIdentity, "/subscriptions/")
vars.UserAssignedIdentity = caseInsensitiveResourceGroupRegexp.ReplaceAllString(vars.UserAssignedIdentity, "/resourceGroups/") vars.UserAssignedIdentity = caseInsensitiveResourceGroupRegexp.ReplaceAllString(vars.UserAssignedIdentity, "/resourceGroups/")
vars.UserAssignedIdentity = caseInsensitiveProvidersRegexp.ReplaceAllString(vars.UserAssignedIdentity, "/providers/") vars.UserAssignedIdentity = caseInsensitiveProvidersRegexp.ReplaceAllString(vars.UserAssignedIdentity, "/providers/")
@ -305,7 +306,7 @@ func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, lv libvirt
Firmware: config.Provider.QEMU.Firmware, Firmware: config.Provider.QEMU.Firmware,
} }
if err := cl.PrepareWorkspace(cloudprovider.QEMU, &vars); err != nil { if err := cl.PrepareWorkspace(path.Join("terraform", strings.ToLower(cloudprovider.QEMU.String())), &vars); err != nil {
return clusterid.File{}, err return clusterid.File{}, err
} }

View File

@ -139,43 +139,43 @@ func TestCreator(t *testing.T) {
func TestNormalizeAzureURIs(t *testing.T) { func TestNormalizeAzureURIs(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
in terraform.AzureVariables in terraform.AzureClusterVariables
want terraform.AzureVariables want terraform.AzureClusterVariables
}{ }{
"empty": { "empty": {
in: terraform.AzureVariables{}, in: terraform.AzureClusterVariables{},
want: terraform.AzureVariables{}, want: terraform.AzureClusterVariables{},
}, },
"no change": { "no change": {
in: terraform.AzureVariables{ in: terraform.AzureClusterVariables{
ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0", ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0",
}, },
want: terraform.AzureVariables{ want: terraform.AzureClusterVariables{
ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0", ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0",
}, },
}, },
"fix image id": { "fix image id": {
in: terraform.AzureVariables{ in: terraform.AzureClusterVariables{
ImageID: "/CommunityGalleries/foo/Images/constellation/Versions/2.1.0", ImageID: "/CommunityGalleries/foo/Images/constellation/Versions/2.1.0",
}, },
want: terraform.AzureVariables{ want: terraform.AzureClusterVariables{
ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0", ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0",
}, },
}, },
"fix resource group": { "fix resource group": {
in: terraform.AzureVariables{ in: terraform.AzureClusterVariables{
UserAssignedIdentity: "/subscriptions/foo/resourcegroups/test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai", UserAssignedIdentity: "/subscriptions/foo/resourcegroups/test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai",
}, },
want: terraform.AzureVariables{ want: terraform.AzureClusterVariables{
UserAssignedIdentity: "/subscriptions/foo/resourceGroups/test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai", UserAssignedIdentity: "/subscriptions/foo/resourceGroups/test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai",
}, },
}, },
"fix arbitrary casing": { "fix arbitrary casing": {
in: terraform.AzureVariables{ in: terraform.AzureClusterVariables{
ImageID: "/CoMMUnitygaLLeries/foo/iMAges/constellation/vERsions/2.1.0", ImageID: "/CoMMUnitygaLLeries/foo/iMAges/constellation/vERsions/2.1.0",
UserAssignedIdentity: "/subsCRiptions/foo/resoURCegroups/test/proViDers/MICROsoft.mANAgedIdentity/USerASsignediDENtities/uai", UserAssignedIdentity: "/subsCRiptions/foo/resoURCegroups/test/proViDers/MICROsoft.mANAgedIdentity/USerASsignediDENtities/uai",
}, },
want: terraform.AzureVariables{ want: terraform.AzureClusterVariables{
ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0", ImageID: "/communityGalleries/foo/images/constellation/versions/2.1.0",
UserAssignedIdentity: "/subscriptions/foo/resourceGroups/test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai", UserAssignedIdentity: "/subscriptions/foo/resourceGroups/test/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai",
}, },

View File

@ -0,0 +1,178 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cloudcmd
import (
"context"
"fmt"
"io"
"path"
"strings"
"github.com/edgelesssys/constellation/v2/cli/internal/iamid"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/constants"
)
// IAMCreator creates the IAM configuration on the cloud provider.
type IAMCreator struct {
out io.Writer
newTerraformClient func(ctx context.Context) (terraformClient, error)
}
// IAMConfig holds the necessary values for IAM configuration.
type IAMConfig struct {
GCP GCPIAMConfig
Azure AzureIAMConfig
AWS AWSIAMConfig
}
// GCPIAMConfig holds the necessary values for GCP IAM configuration.
type GCPIAMConfig struct {
Region string
Zone string
ProjectID string
ServiceAccountID string
}
// AzureIAMConfig holds the necessary values for Azure IAM configuration.
type AzureIAMConfig struct {
Region string
ServicePrincipal string
ResourceGroup string
}
// AWSIAMConfig holds the necessary values for AWS IAM configuration.
type AWSIAMConfig struct {
Region string
Prefix string
}
// NewIAMCreator creates a new IAM creator.
func NewIAMCreator(out io.Writer) *IAMCreator {
return &IAMCreator{
out: out,
newTerraformClient: func(ctx context.Context) (terraformClient, error) {
return terraform.New(ctx, constants.TerraformIAMWorkingDir)
},
}
}
// Create prepares and hands over the corresponding providers IAM creator.
func (c *IAMCreator) Create(ctx context.Context, provider cloudprovider.Provider, iamConfig *IAMConfig) (iamid.File, error) {
switch provider {
case cloudprovider.GCP:
cl, err := c.newTerraformClient(ctx)
if err != nil {
return iamid.File{}, err
}
defer cl.RemoveInstaller()
return c.createGCP(ctx, cl, iamConfig)
case cloudprovider.Azure:
cl, err := c.newTerraformClient(ctx)
if err != nil {
return iamid.File{}, err
}
defer cl.RemoveInstaller()
return c.createAzure(ctx, cl, iamConfig)
case cloudprovider.AWS:
cl, err := c.newTerraformClient(ctx)
if err != nil {
return iamid.File{}, err
}
defer cl.RemoveInstaller()
return c.createAWS(ctx, cl, iamConfig)
default:
return iamid.File{}, fmt.Errorf("unsupported cloud provider: %s", provider)
}
}
// createGCP creates the IAM configuration on GCP.
func (c *IAMCreator) createGCP(ctx context.Context, cl terraformClient, iamConfig *IAMConfig) (retFile iamid.File, retErr error) {
defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerTerraform{client: cl})
vars := terraform.GCPIAMVariables{
ServiceAccountID: iamConfig.GCP.ServiceAccountID,
Project: iamConfig.GCP.ProjectID,
Region: iamConfig.GCP.Region,
Zone: iamConfig.GCP.Zone,
}
if err := cl.PrepareWorkspace(path.Join("terraform", "iam", strings.ToLower(cloudprovider.GCP.String())), &vars); err != nil {
return iamid.File{}, err
}
iamOutput, err := cl.CreateIAMConfig(ctx, cloudprovider.GCP)
if err != nil {
return iamid.File{}, err
}
return iamid.File{
CloudProvider: cloudprovider.GCP,
GCPOutput: iamid.GCPFile{
ServiceAccountKey: iamOutput.GCP.SaKey,
},
}, nil
}
// createAzure creates the IAM configuration on Azure.
func (c *IAMCreator) createAzure(ctx context.Context, cl terraformClient, iamConfig *IAMConfig) (retFile iamid.File, retErr error) {
defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerTerraform{client: cl})
vars := terraform.AzureIAMVariables{
Region: iamConfig.Azure.Region,
ResourceGroup: iamConfig.Azure.ResourceGroup,
ServicePrincipal: iamConfig.Azure.ServicePrincipal,
}
if err := cl.PrepareWorkspace(path.Join("terraform", "iam", strings.ToLower(cloudprovider.Azure.String())), &vars); err != nil {
return iamid.File{}, err
}
iamOutput, err := cl.CreateIAMConfig(ctx, cloudprovider.Azure)
if err != nil {
return iamid.File{}, err
}
return iamid.File{
CloudProvider: cloudprovider.Azure,
AzureOutput: iamid.AzureFile{
ApplicationID: iamOutput.Azure.ApplicationID,
ApplicationClientSecretValue: iamOutput.Azure.ApplicationClientSecretValue,
SubscriptionID: iamOutput.Azure.SubscriptionID,
TenantID: iamOutput.Azure.TenantID,
UAMIID: iamOutput.Azure.UAMIID,
},
}, nil
}
// createAWS creates the IAM configuration on AWS.
func (c *IAMCreator) createAWS(ctx context.Context, cl terraformClient, iamConfig *IAMConfig) (retFile iamid.File, retErr error) {
defer rollbackOnError(context.Background(), c.out, &retErr, &rollbackerTerraform{client: cl})
vars := terraform.AWSIAMVariables{
Region: iamConfig.AWS.Region,
Prefix: iamConfig.AWS.Prefix,
}
if err := cl.PrepareWorkspace(path.Join("terraform", "iam", strings.ToLower(cloudprovider.AWS.String())), &vars); err != nil {
return iamid.File{}, err
}
iamOutput, err := cl.CreateIAMConfig(ctx, cloudprovider.AWS)
if err != nil {
return iamid.File{}, err
}
return iamid.File{
CloudProvider: cloudprovider.AWS,
AWSOutput: iamid.AWSFile{
WorkerNodeInstanceProfile: iamOutput.AWS.WorkerNodeInstanceProfile,
ControlPlaneInstanceProfile: iamOutput.AWS.ControlPlaneInstanceProfile,
},
}, nil
}

View File

@ -0,0 +1,150 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cloudcmd
import (
"bytes"
"context"
"errors"
"testing"
"github.com/edgelesssys/constellation/v2/cli/internal/iamid"
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/stretchr/testify/assert"
)
func TestIAMCreator(t *testing.T) {
someErr := errors.New("failed")
validGCPIAMConfig := GCPIAMConfig{
Region: "europe-west1",
Zone: "europe-west1-a",
ProjectID: "project-1234",
ServiceAccountID: "const-test",
}
validGCPIAMOutput := terraform.IAMOutput{
GCP: terraform.GCPIAMOutput{
SaKey: "not_a_secret",
},
}
validGCPIAMIDFile := iamid.File{
CloudProvider: cloudprovider.GCP,
GCPOutput: iamid.GCPFile{
ServiceAccountKey: "not_a_secret",
},
}
validAzureIAMConfig := AzureIAMConfig{
Region: "westus",
ServicePrincipal: "constell-test",
ResourceGroup: "constell-test",
}
validAzureIAMOutput := terraform.IAMOutput{
Azure: terraform.AzureIAMOutput{
SubscriptionID: "test_subscription_id",
TenantID: "test_tenant_id",
ApplicationID: "test_application_id",
ApplicationClientSecretValue: "test_application_client_secret_value",
UAMIID: "test_uami_id",
},
}
validAzureIAMIDFile := iamid.File{
CloudProvider: cloudprovider.Azure,
AzureOutput: iamid.AzureFile{
SubscriptionID: "test_subscription_id",
TenantID: "test_tenant_id",
ApplicationID: "test_application_id",
ApplicationClientSecretValue: "test_application_client_secret_value",
UAMIID: "test_uami_id",
},
}
validAWSIAMConfig := AWSIAMConfig{
Region: "us-east-2",
Prefix: "test",
}
validAWSIAMOutput := terraform.IAMOutput{
AWS: terraform.AWSIAMOutput{
WorkerNodeInstanceProfile: "test_worker_node_instance_profile",
ControlPlaneInstanceProfile: "test_control_plane_instance_profile",
},
}
validAWSIAMIDFile := iamid.File{
CloudProvider: cloudprovider.AWS,
AWSOutput: iamid.AWSFile{
ControlPlaneInstanceProfile: "test_control_plane_instance_profile",
WorkerNodeInstanceProfile: "test_worker_node_instance_profile",
},
}
testCases := map[string]struct {
tfClient terraformClient
newTfClientErr error
config *IAMConfig
provider cloudprovider.Provider
wantIAMIDFile iamid.File
wantErr bool
}{
"new terraform client err": {
tfClient: &stubTerraformClient{},
newTfClientErr: someErr,
wantErr: true,
},
"create iam config err": {
tfClient: &stubTerraformClient{iamOutputErr: someErr},
wantErr: true,
},
"gcp": {
tfClient: &stubTerraformClient{iamOutput: validGCPIAMOutput},
wantIAMIDFile: validGCPIAMIDFile,
provider: cloudprovider.GCP,
config: &IAMConfig{GCP: validGCPIAMConfig},
},
"azure": {
tfClient: &stubTerraformClient{iamOutput: validAzureIAMOutput},
wantIAMIDFile: validAzureIAMIDFile,
provider: cloudprovider.Azure,
config: &IAMConfig{Azure: validAzureIAMConfig},
},
"aws": {
tfClient: &stubTerraformClient{iamOutput: validAWSIAMOutput},
wantIAMIDFile: validAWSIAMIDFile,
provider: cloudprovider.AWS,
config: &IAMConfig{AWS: validAWSIAMConfig},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
creator := &IAMCreator{
out: &bytes.Buffer{},
newTerraformClient: func(ctx context.Context) (terraformClient, error) {
return tc.tfClient, tc.newTfClientErr
},
}
idFile, err := creator.Create(context.Background(), tc.provider, tc.config)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.provider, idFile.CloudProvider)
switch tc.provider {
case cloudprovider.GCP:
assert.Equal(tc.wantIAMIDFile.GCPOutput, idFile.GCPOutput)
case cloudprovider.Azure:
assert.Equal(tc.wantIAMIDFile.AzureOutput, idFile.AzureOutput)
case cloudprovider.AWS:
assert.Equal(tc.wantIAMIDFile.AWSOutput, idFile.AWSOutput)
}
}
})
}
}

View File

@ -9,7 +9,9 @@ package cmd
import ( import (
"context" "context"
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/cli/internal/clusterid" "github.com/edgelesssys/constellation/v2/cli/internal/clusterid"
"github.com/edgelesssys/constellation/v2/cli/internal/iamid"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/config"
) )
@ -24,6 +26,14 @@ type cloudCreator interface {
) (clusterid.File, error) ) (clusterid.File, error)
} }
type iamCreator interface {
Create(
ctx context.Context,
provider cloudprovider.Provider,
iamConfig *cloudcmd.IAMConfig,
) (iamid.File, error)
}
type cloudTerminator interface { type cloudTerminator interface {
Terminate(context.Context) error Terminate(context.Context) error
} }

View File

@ -10,7 +10,9 @@ import (
"context" "context"
"testing" "testing"
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/cli/internal/clusterid" "github.com/edgelesssys/constellation/v2/cli/internal/clusterid"
"github.com/edgelesssys/constellation/v2/cli/internal/iamid"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/config"
"go.uber.org/goleak" "go.uber.org/goleak"
@ -54,3 +56,19 @@ func (c *stubCloudTerminator) Terminate(context.Context) error {
func (c *stubCloudTerminator) Called() bool { func (c *stubCloudTerminator) Called() bool {
return c.called return c.called
} }
type stubIAMCreator struct {
createCalled bool
id iamid.File
createErr error
}
func (c *stubIAMCreator) Create(
ctx context.Context,
provider cloudprovider.Provider,
iamConfig *cloudcmd.IAMConfig,
) (iamid.File, error) {
c.createCalled = true
c.id.CloudProvider = provider
return c.id, c.createErr
}

View File

@ -27,10 +27,8 @@ func NewCreateCmd() *cobra.Command {
Use: "create", Use: "create",
Short: "Create instances on a cloud platform for your Constellation cluster", Short: "Create instances on a cloud platform for your Constellation cluster",
Long: "Create instances on a cloud platform for your Constellation cluster.", Long: "Create instances on a cloud platform for your Constellation cluster.",
Args: cobra.MatchAll( Args: cobra.ExactArgs(0),
cobra.ExactArgs(0), RunE: runCreate,
),
RunE: runCreate,
} }
cmd.Flags().String("name", "constell", "create the cluster with the specified name") cmd.Flags().String("name", "constell", "create the cluster with the specified name")
cmd.Flags().BoolP("yes", "y", false, "create the cluster without further confirmation") cmd.Flags().BoolP("yes", "y", false, "create the cluster without further confirmation")

40
cli/internal/cmd/iam.go Normal file
View File

@ -0,0 +1,40 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"github.com/spf13/cobra"
)
// NewIAMCmd returns a new cobra.Command for the iam parent command. It needs another verb and does nothing on its own.
func NewIAMCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "iam",
Short: "Work with the IAM configuration on your cloud provider",
Long: "Work with the IAM configuration on your cloud provider.",
Args: cobra.ExactArgs(0),
}
cmd.AddCommand(newIAMCreateCmd())
return cmd
}
// NewIAMCreateCmd returns a new cobra.Command for the iam create parent command. It needs another verb, and does nothing on its own.
func newIAMCreateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Create IAM configuration on a cloud platform for your Constellation cluster",
Long: "Create IAM configuration on a cloud platform for your Constellation cluster.",
Args: cobra.ExactArgs(0),
}
cmd.AddCommand(newIAMCreateAWSCmd())
cmd.AddCommand(newIAMCreateAzureCmd())
cmd.AddCommand(newIAMCreateGCPCmd())
return cmd
}

View File

@ -0,0 +1,128 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"fmt"
"strings"
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/spf13/cobra"
)
// newIAMCreateAWSCmd returns a new cobra.Command for the iam create aws command.
func newIAMCreateAWSCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "aws",
Short: "Create IAM configuration on AWS for your Constellation cluster",
Long: "Create IAM configuration on AWS for your Constellation cluster.",
Args: cobra.ExactArgs(0),
RunE: runIAMCreateAWS,
}
cmd.Flags().String("prefix", "", "Name prefix for all resources.")
must(cobra.MarkFlagRequired(cmd.Flags(), "prefix"))
cmd.Flags().String("zone", "", "AWS availability zone the resources will be created in (e.g. us-east-2a). Find available zones here: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones. Note that we do not support every zone / region. You can find a list of all supported regions in our docs.")
must(cobra.MarkFlagRequired(cmd.Flags(), "zone"))
cmd.Flags().Bool("yes", false, "Create the IAM configuration without further confirmation")
return cmd
}
func runIAMCreateAWS(cmd *cobra.Command, args []string) error {
spinner := newSpinner(cmd.ErrOrStderr())
defer spinner.Stop()
creator := cloudcmd.NewIAMCreator(spinner)
return iamCreateAWS(cmd, spinner, creator)
}
func iamCreateAWS(cmd *cobra.Command, spinner spinnerInterf, creator iamCreator) error {
// Get input variables.
awsFlags, err := parseAWSFlags(cmd)
if err != nil {
return err
}
// Confirmation.
if !awsFlags.yesFlag {
cmd.Printf("The following IAM configuration will be created:\n")
cmd.Printf("Region:\t%s\n", awsFlags.region)
cmd.Printf("Name Prefix:\t%s\n", awsFlags.prefix)
ok, err := askToConfirm(cmd, "Do you want to create the configuration?")
if err != nil {
return err
}
if !ok {
cmd.Println("The creation of the configuration was aborted.")
return nil
}
}
// Creation.
spinner.Start("Creating", false)
iamFile, err := creator.Create(cmd.Context(), cloudprovider.AWS, &cloudcmd.IAMConfig{
AWS: cloudcmd.AWSIAMConfig{
Region: awsFlags.region,
Prefix: awsFlags.prefix,
},
})
spinner.Stop()
if err != nil {
return err
}
cmd.Printf("region:\t%s\n", awsFlags.region)
cmd.Printf("zone:\t%s\n", awsFlags.zone)
cmd.Printf("iamProfileControlPlane:\t%s\n", iamFile.AWSOutput.ControlPlaneInstanceProfile)
cmd.Printf("iamProfileWorkerNodes:\t%s\n", iamFile.AWSOutput.WorkerNodeInstanceProfile)
cmd.Println("Your IAM configuration was created successfully. Please fill the above values into your configuration file.")
return nil
}
// parseAWSFlags parses and validates the flags of the iam create aws command.
func parseAWSFlags(cmd *cobra.Command) (awsFlags, error) {
var region string
prefix, err := cmd.Flags().GetString("prefix")
if err != nil {
return awsFlags{}, fmt.Errorf("parsing prefix string: %w", err)
}
zone, err := cmd.Flags().GetString("zone")
if err != nil {
return awsFlags{}, fmt.Errorf("parsing zone string: %w", err)
}
if strings.HasPrefix(zone, "eu-central-1") {
region = "eu-central-1"
} else if strings.HasPrefix(zone, "us-east-2") {
region = "us-east-2"
} else if strings.HasPrefix(zone, "ap-south-1") {
region = "ap-south-1"
} else {
return awsFlags{}, fmt.Errorf("invalid AWS region, to find a correct region please refer to our docs and https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones")
}
yesFlag, err := cmd.Flags().GetBool("yes")
if err != nil {
return awsFlags{}, fmt.Errorf("parsing yes bool: %w", err)
}
return awsFlags{
zone: zone,
prefix: prefix,
region: region,
yesFlag: yesFlag,
}, nil
}
// awsFlags contains the parsed flags of the iam create aws command.
type awsFlags struct {
prefix string
region string
zone string
yesFlag bool
}

View File

@ -0,0 +1,118 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"bytes"
"testing"
"github.com/edgelesssys/constellation/v2/cli/internal/iamid"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIAMCreateAWS(t *testing.T) {
fsWithDefaultConfig := func(require *require.Assertions, provider cloudprovider.Provider) afero.Fs {
fs := afero.NewMemMapFs()
file := file.NewHandler(fs)
require.NoError(file.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), provider)))
return fs
}
validIAMIDFile := iamid.File{
CloudProvider: cloudprovider.AWS,
AWSOutput: iamid.AWSFile{
ControlPlaneInstanceProfile: "test_control_plane_instance_profile",
WorkerNodeInstanceProfile: "test_worker_nodes_instance_profile",
},
}
testCases := map[string]struct {
setupFs func(*require.Assertions, cloudprovider.Provider) afero.Fs
creator *stubIAMCreator
provider cloudprovider.Provider
zoneFlag string
prefixFlag string
yesFlag bool
stdin string
wantAbort bool
wantErr bool
}{
"iam create aws": {
setupFs: fsWithDefaultConfig,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS,
zoneFlag: "us-east-2a",
prefixFlag: "test",
yesFlag: true,
},
"interactive": {
setupFs: fsWithDefaultConfig,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS,
zoneFlag: "us-east-2a",
prefixFlag: "test",
stdin: "yes\n",
},
"interactive abort": {
setupFs: fsWithDefaultConfig,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS,
zoneFlag: "us-east-2a",
prefixFlag: "test",
stdin: "no\n",
wantAbort: true,
},
"invalid zone": {
setupFs: fsWithDefaultConfig,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.AWS,
zoneFlag: "us-west-5b",
prefixFlag: "test",
yesFlag: true,
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
cmd := newIAMCreateAWSCmd()
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
cmd.SetIn(bytes.NewBufferString(tc.stdin))
if tc.zoneFlag != "" {
require.NoError(cmd.Flags().Set("zone", tc.zoneFlag))
}
if tc.prefixFlag != "" {
require.NoError(cmd.Flags().Set("prefix", tc.prefixFlag))
}
if tc.yesFlag {
require.NoError(cmd.Flags().Set("yes", "true"))
}
err := iamCreateAWS(cmd, nopSpinner{}, tc.creator)
if tc.wantErr {
assert.Error(err)
} else {
if tc.wantAbort {
assert.False(tc.creator.createCalled)
} else {
assert.NoError(err)
assert.True(tc.creator.createCalled)
assert.Equal(tc.creator.id.AWSOutput, validIAMIDFile.AWSOutput)
}
}
})
}
}

View File

@ -0,0 +1,126 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"fmt"
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/spf13/cobra"
)
// newIAMCreateAzureCmd returns a new cobra.Command for the iam create azure command.
func newIAMCreateAzureCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "azure",
Short: "Create IAM configuration on Microsoft Azure for your Constellation cluster",
Long: "Create IAM configuration on Microsoft Azure for your Constellation cluster.",
Args: cobra.ExactArgs(0),
RunE: runIAMCreateAzure,
}
cmd.Flags().String("resourceGroup", "", "Name of the resource group your IAM resources will be created in.")
must(cobra.MarkFlagRequired(cmd.Flags(), "resourceGroup"))
cmd.Flags().String("region", "", "Region the resources will be created in. (e.g. westus)")
must(cobra.MarkFlagRequired(cmd.Flags(), "region"))
cmd.Flags().String("servicePrincipal", "", "Name of the service principal that will be created.")
must(cobra.MarkFlagRequired(cmd.Flags(), "servicePrincipal"))
cmd.Flags().Bool("yes", false, "Create the IAM configuration without further confirmation")
return cmd
}
func runIAMCreateAzure(cmd *cobra.Command, args []string) error {
spinner := newSpinner(cmd.ErrOrStderr())
defer spinner.Stop()
creator := cloudcmd.NewIAMCreator(spinner)
return iamCreateAzure(cmd, spinner, creator)
}
func iamCreateAzure(cmd *cobra.Command, spinner spinnerInterf, creator iamCreator) error {
// Get input variables.
azureFlags, err := parseAzureFlags(cmd)
if err != nil {
return err
}
// Confirmation.
if !azureFlags.yesFlag {
cmd.Printf("The following IAM configuration will be created:\n")
cmd.Printf("Region:\t%s\n", azureFlags.region)
cmd.Printf("Resource Group:\t%s\n", azureFlags.resourceGroup)
cmd.Printf("Service Principal:\t%s\n", azureFlags.servicePrincipal)
ok, err := askToConfirm(cmd, "Do you want to create the configuration?")
if err != nil {
return err
}
if !ok {
cmd.Println("The creation of the configuration was aborted.")
return nil
}
}
// Creation.
spinner.Start("Creating", false)
iamFile, err := creator.Create(cmd.Context(), cloudprovider.Azure, &cloudcmd.IAMConfig{
Azure: cloudcmd.AzureIAMConfig{
Region: azureFlags.region,
ServicePrincipal: azureFlags.servicePrincipal,
ResourceGroup: azureFlags.resourceGroup,
},
})
spinner.Stop()
if err != nil {
return err
}
cmd.Printf("subscription:\t%s\n", iamFile.AzureOutput.SubscriptionID)
cmd.Printf("tenant:\t%s\n", iamFile.AzureOutput.TenantID)
cmd.Printf("location:\t%s\n", azureFlags.region)
cmd.Printf("resourceGroup:\t%s\n", azureFlags.resourceGroup)
cmd.Printf("userAssignedIdentity:\t%s\n", iamFile.AzureOutput.UAMIID)
cmd.Printf("appClientID:\t%s\n", iamFile.AzureOutput.ApplicationID)
cmd.Printf("appClientSecretValue:\t%s\n", iamFile.AzureOutput.ApplicationClientSecretValue)
cmd.Println("Your IAM configuration was created successfully. Please fill the above values into your configuration file.")
return nil
}
// parseAzureFlags parses and validates the flags of the iam create azure command.
func parseAzureFlags(cmd *cobra.Command) (azureFlags, error) {
region, err := cmd.Flags().GetString("region")
if err != nil {
return azureFlags{}, fmt.Errorf("parsing region string: %w", err)
}
resourceGroup, err := cmd.Flags().GetString("resourceGroup")
if err != nil {
return azureFlags{}, fmt.Errorf("parsing resourceGroup string: %w", err)
}
servicePrincipal, err := cmd.Flags().GetString("servicePrincipal")
if err != nil {
return azureFlags{}, fmt.Errorf("parsing servicePrincipal string: %w", err)
}
yesFlag, err := cmd.Flags().GetBool("yes")
if err != nil {
return azureFlags{}, fmt.Errorf("parsing yes bool: %w", err)
}
return azureFlags{
servicePrincipal: servicePrincipal,
resourceGroup: resourceGroup,
region: region,
yesFlag: yesFlag,
}, nil
}
// azureFlags contains the parsed flags of the iam create azure command.
type azureFlags struct {
region string
resourceGroup string
servicePrincipal string
yesFlag bool
}

View File

@ -0,0 +1,119 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"bytes"
"testing"
"github.com/edgelesssys/constellation/v2/cli/internal/iamid"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIAMCreateAzure(t *testing.T) {
fsWithDefaultConfig := func(require *require.Assertions, provider cloudprovider.Provider) afero.Fs {
fs := afero.NewMemMapFs()
file := file.NewHandler(fs)
require.NoError(file.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), provider)))
return fs
}
validIAMIDFile := iamid.File{
CloudProvider: cloudprovider.Azure,
AzureOutput: iamid.AzureFile{
SubscriptionID: "test_subscription_id",
TenantID: "test_tenant_id",
ApplicationID: "test_application_id",
ApplicationClientSecretValue: "test_application_client_secret_value",
UAMIID: "test_uami_id",
},
}
testCases := map[string]struct {
setupFs func(*require.Assertions, cloudprovider.Provider) afero.Fs
creator *stubIAMCreator
provider cloudprovider.Provider
regionFlag string
servicePrincipalFlag string
resourceGroupFlag string
yesFlag bool
stdin string
wantAbort bool
wantErr bool
}{
"iam create azure": {
setupFs: fsWithDefaultConfig,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure,
regionFlag: "westus",
servicePrincipalFlag: "constell-test-sp",
resourceGroupFlag: "constell-test-rg",
yesFlag: true,
},
"interactive": {
setupFs: fsWithDefaultConfig,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure,
regionFlag: "westus",
servicePrincipalFlag: "constell-test-sp",
resourceGroupFlag: "constell-test-rg",
stdin: "yes\n",
},
"interactive abort": {
setupFs: fsWithDefaultConfig,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.Azure,
regionFlag: "westus",
servicePrincipalFlag: "constell-test-sp",
resourceGroupFlag: "constell-test-rg",
stdin: "no\n",
wantAbort: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
cmd := newIAMCreateAzureCmd()
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
cmd.SetIn(bytes.NewBufferString(tc.stdin))
if tc.regionFlag != "" {
require.NoError(cmd.Flags().Set("region", tc.regionFlag))
}
if tc.resourceGroupFlag != "" {
require.NoError(cmd.Flags().Set("resourceGroup", tc.resourceGroupFlag))
}
if tc.servicePrincipalFlag != "" {
require.NoError(cmd.Flags().Set("servicePrincipal", tc.servicePrincipalFlag))
}
if tc.yesFlag {
require.NoError(cmd.Flags().Set("yes", "true"))
}
err := iamCreateAzure(cmd, nopSpinner{}, tc.creator)
if tc.wantErr {
assert.Error(err)
} else {
if tc.wantAbort {
assert.False(tc.creator.createCalled)
} else {
assert.NoError(err)
assert.True(tc.creator.createCalled)
assert.Equal(tc.creator.id.AzureOutput, validIAMIDFile.AzureOutput)
}
}
})
}
}

View File

@ -0,0 +1,183 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"encoding/base64"
"encoding/json"
"fmt"
"regexp"
"strings"
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/spf13/afero"
"github.com/spf13/cobra"
)
var (
zoneRegex = regexp.MustCompile(`^\w+-\w+-[abc]$`)
regionRegex = regexp.MustCompile(`^\w+-\w+[0-9]$`)
projectIDRegex = regexp.MustCompile(`^[a-z][-a-z0-9]{4,28}[a-z0-9]{1}$`)
serviceAccIDRegex = regexp.MustCompile(`^[a-z](?:[-a-z0-9]{4,28}[a-z0-9])$`)
)
// NewIAMCreateGCPCmd returns a new cobra.Command for the iam create gcp command.
func newIAMCreateGCPCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "gcp",
Short: "Create IAM configuration on GCP for your Constellation cluster",
Long: "Create IAM configuration on GCP for your Constellation cluster.",
Args: cobra.ExactArgs(0),
RunE: runIAMCreateGCP,
}
cmd.Flags().String("zone", "", "GCP zone the cluster will be deployed in. Find a list of available zones here: https://cloud.google.com/compute/docs/regions-zones#available")
must(cobra.MarkFlagRequired(cmd.Flags(), "zone"))
cmd.Flags().String("serviceAccountID", "", "ID for the service account that will be created. Must match ^[a-z](?:[-a-z0-9]{4,28}[a-z0-9])$")
must(cobra.MarkFlagRequired(cmd.Flags(), "serviceAccountID"))
cmd.Flags().String("projectID", "", "ID of the GCP project the configuration will be created in. Find it on the welcome screen of your project: https://console.cloud.google.com/welcome")
must(cobra.MarkFlagRequired(cmd.Flags(), "projectID"))
cmd.Flags().Bool("yes", false, "Create the IAM configuration without further confirmation")
return cmd
}
func runIAMCreateGCP(cmd *cobra.Command, args []string) error {
fileHandler := file.NewHandler(afero.NewOsFs())
spinner := newSpinner(cmd.ErrOrStderr())
defer spinner.Stop()
creator := cloudcmd.NewIAMCreator(spinner)
return iamCreateGCP(cmd, spinner, fileHandler, creator)
}
func iamCreateGCP(cmd *cobra.Command, spinner spinnerInterf, fileHandler file.Handler, creator iamCreator) error {
// Get input variables.
gcpFlags, err := parseGCPFlags(cmd)
if err != nil {
return err
}
// Confirmation.
if !gcpFlags.yesFlag {
cmd.Printf("The following IAM configuration will be created:\n")
cmd.Printf("Project ID:\t%s\n", gcpFlags.projectID)
cmd.Printf("Service Account ID:\t%s\n", gcpFlags.serviceAccountID)
cmd.Printf("Region:\t%s\n", gcpFlags.region)
cmd.Printf("Zone:\t%s\n", gcpFlags.zone)
ok, err := askToConfirm(cmd, "Do you want to create the configuration?")
if err != nil {
return err
}
if !ok {
cmd.Println("The creation of the configuration was aborted.")
return nil
}
}
// Creation.
spinner.Start("Creating", false)
iamFile, err := creator.Create(cmd.Context(), cloudprovider.GCP, &cloudcmd.IAMConfig{
GCP: cloudcmd.GCPIAMConfig{
ServiceAccountID: gcpFlags.serviceAccountID,
Region: gcpFlags.region,
Zone: gcpFlags.zone,
ProjectID: gcpFlags.projectID,
},
})
spinner.Stop()
if err != nil {
return err
}
// Write back values.
tmpOut, err := parseIDFile(iamFile.GCPOutput.ServiceAccountKey)
if err != nil {
return err
}
if err := fileHandler.WriteJSON(constants.GCPServiceAccountKeyFile, tmpOut, file.OptNone); err != nil {
return err
}
cmd.Println(fmt.Sprintf("serviceAccountKeyPath:\t%s", constants.GCPServiceAccountKeyFile))
cmd.Println("Your IAM configuration was created successfully. Please fill the above values into your configuration file.")
return nil
}
func parseIDFile(serviceAccountKeyBase64 string) (map[string]string, error) {
dec, err := base64.StdEncoding.DecodeString(serviceAccountKeyBase64)
if err != nil {
return nil, err
}
out := make(map[string]string)
if err = json.Unmarshal(dec, &out); err != nil {
return nil, err
}
return out, nil
}
// parseGCPFlags parses and validates the flags of the iam create gcp command.
func parseGCPFlags(cmd *cobra.Command) (gcpFlags, error) {
zone, err := cmd.Flags().GetString("zone")
if err != nil {
return gcpFlags{}, fmt.Errorf("parsing zone string: %w", err)
}
if !zoneRegex.MatchString(zone) {
return gcpFlags{}, fmt.Errorf("invalid zone string: %s", zone)
}
// Infer region from zone.
zoneParts := strings.Split(zone, "-")
region := fmt.Sprintf("%s-%s", zoneParts[0], zoneParts[1])
if !regionRegex.MatchString(region) {
return gcpFlags{}, fmt.Errorf("invalid region string: %s", region)
}
projectID, err := cmd.Flags().GetString("projectID")
if err != nil {
return gcpFlags{}, fmt.Errorf("parsing projectID string: %w", err)
}
// Source for regex: https://cloud.google.com/resource-manager/reference/rest/v1/projects.
if !projectIDRegex.MatchString(projectID) {
return gcpFlags{}, fmt.Errorf("invalid projectID string: %s", projectID)
}
serviceAccID, err := cmd.Flags().GetString("serviceAccountID")
if err != nil {
return gcpFlags{}, fmt.Errorf("parsing serviceAccountID string: %w", err)
}
if !serviceAccIDRegex.MatchString(serviceAccID) {
return gcpFlags{}, fmt.Errorf("invalid serviceAccountID string: %s", serviceAccID)
}
yesFlag, err := cmd.Flags().GetBool("yes")
if err != nil {
return gcpFlags{}, fmt.Errorf("parsing yes bool: %w", err)
}
return gcpFlags{
serviceAccountID: serviceAccID,
zone: zone,
region: region,
projectID: projectID,
yesFlag: yesFlag,
}, nil
}
// gcpFlags contains the parsed flags of the iam create gcp command.
type gcpFlags struct {
serviceAccountID string
zone string
region string
projectID string
yesFlag bool
}

View File

@ -0,0 +1,184 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package cmd
import (
"bytes"
"testing"
"github.com/edgelesssys/constellation/v2/cli/internal/iamid"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/file"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIAMCreateGCP(t *testing.T) {
fsWithDefaultConfig := func(require *require.Assertions, provider cloudprovider.Provider) afero.Fs {
fs := afero.NewMemMapFs()
file := file.NewHandler(fs)
require.NoError(file.WriteYAML(constants.ConfigFilename, defaultConfigWithExpectedMeasurements(t, config.Default(), provider)))
return fs
}
validIAMIDFile := iamid.File{
CloudProvider: cloudprovider.GCP,
GCPOutput: iamid.GCPFile{
ServiceAccountKey: "eyJwcml2YXRlX2tleV9pZCI6Im5vdF9hX3NlY3JldCJ9Cg==", // {"private_key_id":"not_a_secret"}
},
}
invalidIAMIDFile := iamid.File{
CloudProvider: cloudprovider.GCP,
GCPOutput: iamid.GCPFile{
ServiceAccountKey: "ey_Jwcml2YXRlX2tleV9pZCI6Im5vdF9hX3NlY3JldCJ9Cg==", // invalid b64
},
}
testCases := map[string]struct {
setupFs func(*require.Assertions, cloudprovider.Provider) afero.Fs
creator *stubIAMCreator
provider cloudprovider.Provider
zoneFlag string
serviceAccountIDFlag string
projectIDFlag string
yesFlag bool
stdin string
wantAbort bool
wantErr bool
}{
"iam create gcp": {
setupFs: fsWithDefaultConfig,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
yesFlag: true,
},
"iam create gcp invalid flags": {
setupFs: fsWithDefaultConfig,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "-a",
yesFlag: true,
wantErr: true,
},
"iam create gcp invalid b64": {
setupFs: fsWithDefaultConfig,
creator: &stubIAMCreator{id: invalidIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
yesFlag: true,
wantErr: true,
},
"interactive": {
setupFs: fsWithDefaultConfig,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
stdin: "yes\n",
},
"interactive abort": {
setupFs: fsWithDefaultConfig,
creator: &stubIAMCreator{id: validIAMIDFile},
provider: cloudprovider.GCP,
zoneFlag: "europe-west1-a",
serviceAccountIDFlag: "constell-test",
projectIDFlag: "constell-1234",
stdin: "no\n",
wantAbort: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
cmd := newIAMCreateGCPCmd()
cmd.SetOut(&bytes.Buffer{})
cmd.SetErr(&bytes.Buffer{})
cmd.SetIn(bytes.NewBufferString(tc.stdin))
if tc.zoneFlag != "" {
require.NoError(cmd.Flags().Set("zone", tc.zoneFlag))
}
if tc.serviceAccountIDFlag != "" {
require.NoError(cmd.Flags().Set("serviceAccountID", tc.serviceAccountIDFlag))
}
if tc.projectIDFlag != "" {
require.NoError(cmd.Flags().Set("projectID", tc.projectIDFlag))
}
if tc.yesFlag {
require.NoError(cmd.Flags().Set("yes", "true"))
}
fileHandler := file.NewHandler(tc.setupFs(require, tc.provider))
err := iamCreateGCP(cmd, nopSpinner{}, fileHandler, tc.creator)
if tc.wantErr {
assert.Error(err)
} else {
if tc.wantAbort {
assert.False(tc.creator.createCalled)
} else {
assert.NoError(err)
assert.True(tc.creator.createCalled)
assert.Equal(tc.creator.id.GCPOutput, validIAMIDFile.GCPOutput)
}
}
})
}
}
func TestParseIDFile(t *testing.T) {
validIAMIDFile := iamid.File{
CloudProvider: cloudprovider.GCP,
GCPOutput: iamid.GCPFile{
ServiceAccountKey: "eyJwcml2YXRlX2tleV9pZCI6Im5vdF9hX3NlY3JldCJ9Cg==", // {"private_key_id":"not_a_secret"}
},
}
invalidIAMIDFile := iamid.File{
CloudProvider: cloudprovider.GCP,
GCPOutput: iamid.GCPFile{
ServiceAccountKey: "ey_Jwcml2YXRlX2tleV9pZCI6Im5vdF9hX3NlY3JldCJ9Cg==", // invalid b64
},
}
testCases := map[string]struct {
idFile iamid.File
wantPrivateKeyID string
wantErr bool
}{
"valid base64": {
idFile: validIAMIDFile,
wantPrivateKeyID: "not_a_secret",
},
"invalid base64": {
idFile: invalidIAMIDFile,
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
outMap, err := parseIDFile(tc.idFile.GCPOutput.ServiceAccountKey)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.wantPrivateKeyID, outMap["private_key_id"])
}
})
}
}

View File

@ -38,9 +38,7 @@ func NewVerifyCmd() *cobra.Command {
Long: `Verify the confidential properties of a Constellation cluster. Long: `Verify the confidential properties of a Constellation cluster.
If arguments aren't specified, values are read from ` + "`" + constants.ClusterIDsFileName + "`.", If arguments aren't specified, values are read from ` + "`" + constants.ClusterIDsFileName + "`.",
Args: cobra.MatchAll( Args: cobra.ExactArgs(0),
cobra.ExactArgs(0),
),
RunE: runVerify, RunE: runVerify,
} }
cmd.Flags().String("cluster-id", "", "expected cluster identifier") cmd.Flags().String("cluster-id", "", "expected cluster identifier")

42
cli/internal/iamid/id.go Normal file
View File

@ -0,0 +1,42 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package iamid
import (
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
)
// File contains output information of an IAM configuration.
type File struct {
// CloudProvider is the cloud provider of the cluster.
CloudProvider cloudprovider.Provider `json:"cloudprovider,omitempty"`
GCPOutput GCPFile `json:"gcpOutput,omitempty"`
AzureOutput AzureFile `json:"azureOutput,omitempty"`
AWSOutput AWSFile `json:"awsOutput,omitempty"`
}
// GCPFile contains the output information of a GCP IAM configuration.
type GCPFile struct {
ServiceAccountKey string `json:"serviceAccountID,omitempty"`
}
// AzureFile contains the output information of a Microsoft Azure IAM configuration.
type AzureFile struct {
SubscriptionID string `json:"subscriptionID,omitempty"`
TenantID string `json:"tenantID,omitempty"`
ApplicationID string `json:"applicationID,omitempty"`
UAMIID string `json:"uamiID,omitempty"`
ApplicationClientSecretValue string `json:"applicationClientSecretValue,omitempty"`
}
// AWSFile contains the output information of an AWS IAM configuration.
type AWSFile struct {
ControlPlaneInstanceProfile string `json:"controlPlaneInstanceProfile,omitempty"`
WorkerNodeInstanceProfile string `json:"workerNodeInstanceProfile,omitempty"`
}

View File

@ -11,11 +11,9 @@ import (
"embed" "embed"
"errors" "errors"
"io/fs" "io/fs"
"path"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/file" "github.com/edgelesssys/constellation/v2/internal/file"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
@ -29,9 +27,8 @@ var terraformFS embed.FS
// prepareWorkspace loads the embedded Terraform files, // prepareWorkspace loads the embedded Terraform files,
// and writes them into the workspace. // and writes them into the workspace.
func prepareWorkspace(fileHandler file.Handler, provider cloudprovider.Provider, workingDir string) error { func prepareWorkspace(path string, fileHandler file.Handler, workingDir string) error {
// use path.Join to ensure no forward slashes are used to read the embedded FS rootDir := path
rootDir := path.Join("terraform", strings.ToLower(provider.String()))
return fs.WalkDir(terraformFS, rootDir, func(path string, d fs.DirEntry, err error) error { return fs.WalkDir(terraformFS, rootDir, func(path string, d fs.DirEntry, err error) error {
if err != nil { if err != nil {
return err return err

View File

@ -8,7 +8,9 @@ package terraform
import ( import (
"io/fs" "io/fs"
"path"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
@ -21,11 +23,13 @@ import (
func TestLoader(t *testing.T) { func TestLoader(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
pathBase string
provider cloudprovider.Provider provider cloudprovider.Provider
fileList []string fileList []string
testAlreadyUnpacked bool testAlreadyUnpacked bool
}{ }{
"aws": { "awsCluster": {
pathBase: "terraform",
provider: cloudprovider.AWS, provider: cloudprovider.AWS,
fileList: []string{ fileList: []string{
"main.tf", "main.tf",
@ -34,7 +38,8 @@ func TestLoader(t *testing.T) {
"modules", "modules",
}, },
}, },
"gcp": { "gcpCluster": {
pathBase: "terraform",
provider: cloudprovider.GCP, provider: cloudprovider.GCP,
fileList: []string{ fileList: []string{
"main.tf", "main.tf",
@ -43,7 +48,8 @@ func TestLoader(t *testing.T) {
"modules", "modules",
}, },
}, },
"qemu": { "qemuCluster": {
pathBase: "terraform",
provider: cloudprovider.QEMU, provider: cloudprovider.QEMU,
fileList: []string{ fileList: []string{
"main.tf", "main.tf",
@ -52,7 +58,35 @@ func TestLoader(t *testing.T) {
"modules", "modules",
}, },
}, },
"gcpIAM": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.GCP,
fileList: []string{
"main.tf",
"variables.tf",
"outputs.tf",
},
},
"azureIAM": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.Azure,
fileList: []string{
"main.tf",
"variables.tf",
"outputs.tf",
},
},
"awsIAM": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.AWS,
fileList: []string{
"main.tf",
"variables.tf",
"outputs.tf",
},
},
"continue on (partially) unpacked": { "continue on (partially) unpacked": {
pathBase: "terraform",
provider: cloudprovider.AWS, provider: cloudprovider.AWS,
fileList: []string{ fileList: []string{
"main.tf", "main.tf",
@ -71,14 +105,16 @@ func TestLoader(t *testing.T) {
file := file.NewHandler(afero.NewMemMapFs()) file := file.NewHandler(afero.NewMemMapFs())
err := prepareWorkspace(file, tc.provider, constants.TerraformWorkingDir) path := path.Join(tc.pathBase, strings.ToLower(tc.provider.String()))
err := prepareWorkspace(path, file, constants.TerraformWorkingDir)
require.NoError(err) require.NoError(err)
checkFiles(t, file, func(err error) { assert.NoError(err) }, tc.fileList) checkFiles(t, file, func(err error) { assert.NoError(err) }, tc.fileList)
if tc.testAlreadyUnpacked { if tc.testAlreadyUnpacked {
// Let's try the same again and check if we don't get a "file already exists" error. // Let's try the same again and check if we don't get a "file already exists" error.
require.NoError(file.Remove(filepath.Join(constants.TerraformWorkingDir, "variables.tf"))) require.NoError(file.Remove(filepath.Join(constants.TerraformWorkingDir, "variables.tf")))
err := prepareWorkspace(file, tc.provider, constants.TerraformWorkingDir) err := prepareWorkspace(path, file, constants.TerraformWorkingDir)
assert.NoError(err) assert.NoError(err)
checkFiles(t, file, func(err error) { assert.NoError(err) }, tc.fileList) checkFiles(t, file, func(err error) { assert.NoError(err) }, tc.fileList)
} }

View File

@ -61,8 +61,8 @@ func New(ctx context.Context, workingDir string) (*Client, error) {
} }
// PrepareWorkspace prepares a Terraform workspace for a Constellation cluster. // PrepareWorkspace prepares a Terraform workspace for a Constellation cluster.
func (c *Client) PrepareWorkspace(provider cloudprovider.Provider, vars Variables) error { func (c *Client) PrepareWorkspace(path string, vars Variables) error {
if err := prepareWorkspace(c.file, provider, c.workingDir); err != nil { if err := prepareWorkspace(path, c.file, c.workingDir); err != nil {
return err return err
} }
@ -109,6 +109,141 @@ func (c *Client) CreateCluster(ctx context.Context) (string, string, error) {
return ip, secret, nil return ip, secret, nil
} }
// IAMOutput contains the output information of the Terraform IAM operations.
type IAMOutput struct {
GCP GCPIAMOutput
Azure AzureIAMOutput
AWS AWSIAMOutput
}
// GCPIAMOutput contains the output information of the Terraform IAM operation on GCP.
type GCPIAMOutput struct {
SaKey string
}
// AzureIAMOutput contains the output information of the Terraform IAM operation on Microsoft Azure.
type AzureIAMOutput struct {
SubscriptionID string
TenantID string
ApplicationID string
UAMIID string
ApplicationClientSecretValue string
}
// AWSIAMOutput contains the output information of the Terraform IAM operation on GCP.
type AWSIAMOutput struct {
ControlPlaneInstanceProfile string
WorkerNodeInstanceProfile string
}
// CreateIAMConfig creates an IAM configuration using Terraform.
func (c *Client) CreateIAMConfig(ctx context.Context, provider cloudprovider.Provider) (IAMOutput, error) {
if err := c.tf.Init(ctx); err != nil {
return IAMOutput{}, err
}
if err := c.tf.Apply(ctx); err != nil {
return IAMOutput{}, err
}
tfState, err := c.tf.Show(ctx)
if err != nil {
return IAMOutput{}, err
}
switch provider {
case cloudprovider.GCP:
saKeyOutputRaw, ok := tfState.Values.Outputs["sa_key"]
if !ok {
return IAMOutput{}, errors.New("no service account key output found")
}
saKeyOutput, ok := saKeyOutputRaw.Value.(string)
if !ok {
return IAMOutput{}, errors.New("invalid type in service account key output: not a string")
}
return IAMOutput{
GCP: GCPIAMOutput{
SaKey: saKeyOutput,
},
}, nil
case cloudprovider.Azure:
subscriptionIDRaw, ok := tfState.Values.Outputs["subscription_id"]
if !ok {
return IAMOutput{}, errors.New("no subscription id output found")
}
subscriptionIDOutput, ok := subscriptionIDRaw.Value.(string)
if !ok {
return IAMOutput{}, errors.New("invalid type in subscription id output: not a string")
}
tenantIDRaw, ok := tfState.Values.Outputs["tenant_id"]
if !ok {
return IAMOutput{}, errors.New("no tenant id output found")
}
tenantIDOutput, ok := tenantIDRaw.Value.(string)
if !ok {
return IAMOutput{}, errors.New("invalid type in tenant id output: not a string")
}
applicationIDRaw, ok := tfState.Values.Outputs["application_id"]
if !ok {
return IAMOutput{}, errors.New("no application id output found")
}
applicationIDOutput, ok := applicationIDRaw.Value.(string)
if !ok {
return IAMOutput{}, errors.New("invalid type in application id output: not a string")
}
uamiIDRaw, ok := tfState.Values.Outputs["uami_id"]
if !ok {
return IAMOutput{}, errors.New("no UAMI id output found")
}
uamiIDOutput, ok := uamiIDRaw.Value.(string)
if !ok {
return IAMOutput{}, errors.New("invalid type in UAMI id output: not a string")
}
appClientSecretRaw, ok := tfState.Values.Outputs["application_client_secret_value"]
if !ok {
return IAMOutput{}, errors.New("no application client secret value output found")
}
appClientSecretOutput, ok := appClientSecretRaw.Value.(string)
if !ok {
return IAMOutput{}, errors.New("invalid type in application client secret valueoutput: not a string")
}
return IAMOutput{
Azure: AzureIAMOutput{
SubscriptionID: subscriptionIDOutput,
TenantID: tenantIDOutput,
ApplicationID: applicationIDOutput,
UAMIID: uamiIDOutput,
ApplicationClientSecretValue: appClientSecretOutput,
},
}, nil
case cloudprovider.AWS:
controlPlaneProfileRaw, ok := tfState.Values.Outputs["control_plane_instance_profile"]
if !ok {
return IAMOutput{}, errors.New("no control plane instance profile output found")
}
controlPlaneProfileOutput, ok := controlPlaneProfileRaw.Value.(string)
if !ok {
return IAMOutput{}, errors.New("invalid type in control plane instance profile output: not a string")
}
workerNodeProfileRaw, ok := tfState.Values.Outputs["worker_nodes_instance_profile"]
if !ok {
return IAMOutput{}, errors.New("no worker node instance profile output found")
}
workerNodeProfileOutput, ok := workerNodeProfileRaw.Value.(string)
if !ok {
return IAMOutput{}, errors.New("invalid type in worker node instance profile output: not a string")
}
return IAMOutput{
AWS: AWSIAMOutput{
ControlPlaneInstanceProfile: controlPlaneProfileOutput,
WorkerNodeInstanceProfile: workerNodeProfileOutput,
},
}, nil
default:
return IAMOutput{}, errors.New("unsupported cloud provider")
}
}
// DestroyCluster destroys a Constellation cluster using Terraform. // DestroyCluster destroys a Constellation cluster using Terraform.
func (c *Client) DestroyCluster(ctx context.Context) error { func (c *Client) DestroyCluster(ctx context.Context) error {
if err := c.tf.Init(ctx); err != nil { if err := c.tf.Init(ctx); err != nil {

View File

@ -6,14 +6,6 @@ output "tenant_id" {
value = data.azurerm_subscription.current.tenant_id value = data.azurerm_subscription.current.tenant_id
} }
output "region" {
value = var.region
}
output "base_resource_group_name" {
value = var.resource_group_name
}
output "application_id" { output "application_id" {
value = azuread_application.base_application.application_id value = azuread_application.base_application.application_id
} }

View File

@ -0,0 +1,42 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/google" {
version = "4.44.0"
constraints = "4.44.0"
hashes = [
"h1:JpShTtgnxpiIVnr0R2Lccrh84mnrf7Z1/v/yw0UZ1gI=",
"h1:JzDVTkBVHTw67N7S9bD/+u7U3aipjutF4MZx0eplONw=",
"h1:OTNLlWoTq+SdbbtgLK7uFVAn3aP9QVjIGYU2ndKsz+Q=",
"h1:PSIkDVwksHe9oZd+XP369N8U+6/+SPF8Z5wHkcwmWKw=",
"h1:d6Ds7aGeW5M8+AgWfkfbh3HTf3tP8ePjYGhoJnOxklU=",
"h1:enGPHgPHCbmLU8BIHLHcOwRlbQnPPQzdmXNoFVs1lIg=",
"h1:gmUUWhuuY/YRIllvVBRGl1kUHqsNBHQ/4BHdwKQbzXQ=",
"h1:hMhGerpfzsbfQ3DFsAGwJYpSIK6an+wMGx2I/lwaNuY=",
"h1:iotFu763eGWVC4zfXkNuo3RUeQLDvwVX2E7R/2sNxCg=",
"h1:mheXqRMLbMeTr8/E6UakMhWwIL0HqwIHYBE2u2Sbldg=",
"zh:0b424cab24856dc47177733145fa61b731f345a6a42a0c0b7910ccfcf4e8c8a2",
"zh:0c6b3049957b942e1dbc6f8c38de653a78ff1efe40a7cfc506e60a8da2775591",
"zh:1e1d282abd9dbd30602ca52050f875deeb4d79b3b06cda69535db4863f1b98a0",
"zh:35b7bc7c4da98e47bc7827fe1c39400811c9af459e03c99217195830f6bb0e78",
"zh:5ea45d229afad298bdac80324ec9d6968443d54ed1d1a01fad70b3c1bd16e53b",
"zh:652b740a7f75d716daf0fa9b2ef1964944eb4f8b0b26834dd8659a6ac2f3ed52",
"zh:75ee47f9a28826de301c3c6d2fb7915414f3841b1d0248cad6b94697fb8ee634",
"zh:89222d36d8060beb13df6758d6d9b2d075fa809e90a910a2ce1a867cfa6ff654",
"zh:a8c04acc69a65cb68b91ec08aa89c4953840dad33482c9acf4cc0272375b3bf4",
"zh:b71c10a8167cb6c7c3ae174c8c181a06dc82564f097f89602c3d74e8a7627e92",
"zh:bb9a92b640cf0596edcc510ddd20725637c1ff295054f727277108a4a3c9baec",
"zh:bccae696aa84ff7978c6a81b25d61f722fc87261e4b524ec062392916c8494ab",
"zh:bcd028cd233287420ecfbe4102e59e351e6fd22a4a14698e6896c45fb0509a1e",
"zh:bd9d096abdc42a3cf5849ae8adc9c8ca327c026e6f6f287fd436b6adfc8630dc",
"zh:c3577ea597c9f8707b86a3056f59ff28c35c77b17392dd75a5dbe46617f7f7c6",
"zh:d44915d60ffb54edf48ff9a1c2661b358497a9ecb900e8a3b3983f083ea198ce",
"zh:d5dfcc1949e12fcc2d9ac834296872bded5b4201f2c492eb127872a72080b56b",
"zh:d9dc91823cbcd60464d5b168d7bd34a5b5f13d5b37786110226bfe139ea24782",
"zh:e8647c8ab63144013446b73c695a01f6bef16712613f1461d1c0bc37e1ba80d6",
"zh:ecd0cd61f13d401240301691f3042abca874a5c481a90b4f791c0e4ea10b6448",
"zh:ed01ea31e457d6c4e01a5d6dfd6ad3d09a0a58ff7dc4de494bf559fbc34fa936",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
"zh:ffb268a1b2b401175667a7e421100ce8964f6b32cab723766e8eb0b993b59e66",
]
}

View File

@ -2,7 +2,7 @@ terraform {
required_providers { required_providers {
google = { google = {
source = "hashicorp/google" source = "hashicorp/google"
version = "4.44.1" version = "4.44.0"
} }
} }
} }

View File

@ -10,7 +10,9 @@ import (
"context" "context"
"errors" "errors"
"io/fs" "io/fs"
"path"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
@ -40,6 +42,7 @@ func TestPrepareCluster(t *testing.T) {
} }
testCases := map[string]struct { testCases := map[string]struct {
pathBase string
provider cloudprovider.Provider provider cloudprovider.Provider
vars Variables vars Variables
fs afero.Fs fs afero.Fs
@ -47,17 +50,20 @@ func TestPrepareCluster(t *testing.T) {
wantErr bool wantErr bool
}{ }{
"qemu": { "qemu": {
pathBase: "terraform",
provider: cloudprovider.QEMU, provider: cloudprovider.QEMU,
vars: qemuVars, vars: qemuVars,
fs: afero.NewMemMapFs(), fs: afero.NewMemMapFs(),
wantErr: false, wantErr: false,
}, },
"no vars": { "no vars": {
pathBase: "terraform",
provider: cloudprovider.QEMU, provider: cloudprovider.QEMU,
fs: afero.NewMemMapFs(), fs: afero.NewMemMapFs(),
wantErr: true, wantErr: true,
}, },
"continue on partially extracted": { "continue on partially extracted": {
pathBase: "terraform",
provider: cloudprovider.QEMU, provider: cloudprovider.QEMU,
vars: qemuVars, vars: qemuVars,
fs: afero.NewMemMapFs(), fs: afero.NewMemMapFs(),
@ -65,6 +71,7 @@ func TestPrepareCluster(t *testing.T) {
wantErr: false, wantErr: false,
}, },
"prepare workspace fails": { "prepare workspace fails": {
pathBase: "terraform",
provider: cloudprovider.QEMU, provider: cloudprovider.QEMU,
vars: qemuVars, vars: qemuVars,
fs: afero.NewReadOnlyFs(afero.NewMemMapFs()), fs: afero.NewReadOnlyFs(afero.NewMemMapFs()),
@ -83,12 +90,107 @@ func TestPrepareCluster(t *testing.T) {
workingDir: constants.TerraformWorkingDir, workingDir: constants.TerraformWorkingDir,
} }
err := c.PrepareWorkspace(tc.provider, tc.vars) 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. // Test case: Check if we can continue to create on an incomplete workspace.
if tc.partiallyExtracted { if tc.partiallyExtracted {
require.NoError(c.file.Remove(filepath.Join(c.workingDir, "main.tf"))) require.NoError(c.file.Remove(filepath.Join(c.workingDir, "main.tf")))
err = c.PrepareWorkspace(tc.provider, tc.vars) 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{
Region: "westus",
ServicePrincipal: "constell-test-sp",
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("terraform", "iam"),
fs: afero.NewMemMapFs(),
wantErr: true,
},
"invalid path": {
pathBase: path.Join("abc", "123"),
fs: afero.NewMemMapFs(),
wantErr: true,
},
"gcp": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.GCP,
vars: gcpVars,
fs: afero.NewMemMapFs(),
wantErr: false,
},
"continue on partially extracted": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.GCP,
vars: gcpVars,
fs: afero.NewMemMapFs(),
partiallyExtracted: true,
wantErr: false,
},
"azure": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.Azure,
vars: azureVars,
fs: afero.NewMemMapFs(),
wantErr: false,
},
"aws": {
pathBase: path.Join("terraform", "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 { if tc.wantErr {
@ -132,6 +234,7 @@ func TestCreateCluster(t *testing.T) {
} }
testCases := map[string]struct { testCases := map[string]struct {
pathBase string
provider cloudprovider.Provider provider cloudprovider.Provider
vars Variables vars Variables
tf *stubTerraform tf *stubTerraform
@ -139,12 +242,14 @@ func TestCreateCluster(t *testing.T) {
wantErr bool wantErr bool
}{ }{
"works": { "works": {
pathBase: "terraform",
provider: cloudprovider.QEMU, provider: cloudprovider.QEMU,
vars: qemuVars, vars: qemuVars,
tf: &stubTerraform{showState: newTestState()}, tf: &stubTerraform{showState: newTestState()},
fs: afero.NewMemMapFs(), fs: afero.NewMemMapFs(),
}, },
"init fails": { "init fails": {
pathBase: "terraform",
provider: cloudprovider.QEMU, provider: cloudprovider.QEMU,
vars: qemuVars, vars: qemuVars,
tf: &stubTerraform{initErr: someErr}, tf: &stubTerraform{initErr: someErr},
@ -152,6 +257,7 @@ func TestCreateCluster(t *testing.T) {
wantErr: true, wantErr: true,
}, },
"apply fails": { "apply fails": {
pathBase: "terraform",
provider: cloudprovider.QEMU, provider: cloudprovider.QEMU,
vars: qemuVars, vars: qemuVars,
tf: &stubTerraform{applyErr: someErr}, tf: &stubTerraform{applyErr: someErr},
@ -159,6 +265,7 @@ func TestCreateCluster(t *testing.T) {
wantErr: true, wantErr: true,
}, },
"show fails": { "show fails": {
pathBase: "terraform",
provider: cloudprovider.QEMU, provider: cloudprovider.QEMU,
vars: qemuVars, vars: qemuVars,
tf: &stubTerraform{showErr: someErr}, tf: &stubTerraform{showErr: someErr},
@ -166,6 +273,7 @@ func TestCreateCluster(t *testing.T) {
wantErr: true, wantErr: true,
}, },
"no ip": { "no ip": {
pathBase: "terraform",
provider: cloudprovider.QEMU, provider: cloudprovider.QEMU,
vars: qemuVars, vars: qemuVars,
tf: &stubTerraform{ tf: &stubTerraform{
@ -179,6 +287,7 @@ func TestCreateCluster(t *testing.T) {
wantErr: true, wantErr: true,
}, },
"ip has wrong type": { "ip has wrong type": {
pathBase: "terraform",
provider: cloudprovider.QEMU, provider: cloudprovider.QEMU,
vars: qemuVars, vars: qemuVars,
tf: &stubTerraform{ tf: &stubTerraform{
@ -204,7 +313,8 @@ func TestCreateCluster(t *testing.T) {
workingDir: constants.TerraformWorkingDir, workingDir: constants.TerraformWorkingDir,
} }
require.NoError(c.PrepareWorkspace(tc.provider, tc.vars)) path := path.Join(tc.pathBase, strings.ToLower(tc.provider.String()))
require.NoError(c.PrepareWorkspace(path, tc.vars))
ip, initSecret, err := c.CreateCluster(context.Background()) ip, initSecret, err := c.CreateCluster(context.Background())
if tc.wantErr { if tc.wantErr {
@ -218,6 +328,282 @@ func TestCreateCluster(t *testing.T) {
} }
} }
func TestCreateIAM(t *testing.T) {
someErr := errors.New("failed")
newTestState := func() *tfjson.State {
workingState := tfjson.State{
Values: &tfjson.StateValues{
Outputs: map[string]*tfjson.StateOutput{
"sa_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",
},
"control_plane_instance_profile": {
Value: "test_control_plane_instance_profile",
},
"worker_nodes_instance_profile": {
Value: "test_worker_nodes_instance_profile",
},
},
},
}
return &workingState
}
gcpVars := &GCPIAMVariables{
Project: "const-1234",
Region: "europe-west1",
Zone: "europe-west1-a",
ServiceAccountID: "const-test-case",
}
azureVars := &AzureIAMVariables{
Region: "westus",
ServicePrincipal: "constell-test-sp",
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
}{
"gcp works": {
pathBase: path.Join("terraform", "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("terraform", "iam"),
provider: cloudprovider.GCP,
vars: gcpVars,
tf: &stubTerraform{initErr: someErr},
fs: afero.NewMemMapFs(),
wantErr: true,
},
"gcp apply fails": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.GCP,
vars: gcpVars,
tf: &stubTerraform{applyErr: someErr},
fs: afero.NewMemMapFs(),
wantErr: true,
},
"gcp show fails": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.GCP,
vars: gcpVars,
tf: &stubTerraform{showErr: someErr},
fs: afero.NewMemMapFs(),
wantErr: true,
},
"gcp no sa_key": {
pathBase: path.Join("terraform", "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 sa_key has wrong type": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.GCP,
vars: gcpVars,
tf: &stubTerraform{
showState: &tfjson.State{
Values: &tfjson.StateValues{
Outputs: map[string]*tfjson.StateOutput{"sa_key": {Value: 42}},
},
},
},
fs: afero.NewMemMapFs(),
wantErr: true,
},
"azure works": {
pathBase: path.Join("terraform", "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",
ApplicationID: "test_application_id",
ApplicationClientSecretValue: "test_application_client_secret_value",
UAMIID: "test_uami_id",
}},
},
"azure init fails": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.Azure,
vars: azureVars,
tf: &stubTerraform{initErr: someErr},
fs: afero.NewMemMapFs(),
wantErr: true,
},
"azure apply fails": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.Azure,
vars: azureVars,
tf: &stubTerraform{applyErr: someErr},
fs: afero.NewMemMapFs(),
wantErr: true,
},
"azure show fails": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.Azure,
vars: azureVars,
tf: &stubTerraform{showErr: someErr},
fs: afero.NewMemMapFs(),
wantErr: true,
},
"azure no subscription_id": {
pathBase: path.Join("terraform", "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("terraform", "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("terraform", "iam"),
provider: cloudprovider.AWS,
vars: awsVars,
tf: &stubTerraform{showState: newTestState()},
fs: afero.NewMemMapFs(),
want: IAMOutput{AWS: AWSIAMOutput{
ControlPlaneInstanceProfile: "test_control_plane_instance_profile",
WorkerNodeInstanceProfile: "test_worker_nodes_instance_profile",
}},
},
"aws init fails": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.AWS,
vars: awsVars,
tf: &stubTerraform{initErr: someErr},
fs: afero.NewMemMapFs(),
wantErr: true,
},
"aws apply fails": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.AWS,
vars: awsVars,
tf: &stubTerraform{applyErr: someErr},
fs: afero.NewMemMapFs(),
wantErr: true,
},
"aws show fails": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.AWS,
vars: awsVars,
tf: &stubTerraform{showErr: someErr},
fs: afero.NewMemMapFs(),
wantErr: true,
},
"aws no control_plane_instance_profile": {
pathBase: path.Join("terraform", "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 control_plane_instance_profile has wrong type": {
pathBase: path.Join("terraform", "iam"),
provider: cloudprovider.AWS,
vars: awsVars,
tf: &stubTerraform{
showState: &tfjson.State{
Values: &tfjson.StateValues{
Outputs: map[string]*tfjson.StateOutput{"control_plane_instance_profile": {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.CreateIAMConfig(context.Background(), tc.provider)
if tc.wantErr {
assert.Error(err)
return
}
assert.NoError(err)
assert.Equal(tc.want, IAMoutput)
})
}
}
func TestDestroyInstances(t *testing.T) { func TestDestroyInstances(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
tf *stubTerraform tf *stubTerraform

View File

@ -39,8 +39,8 @@ func (v *CommonVariables) String() string {
return b.String() return b.String()
} }
// AWSVariables is user configuration for creating a cluster with Terraform on GCP. // AWSClusterVariables is user configuration for creating a cluster with Terraform on GCP.
type AWSVariables struct { type AWSClusterVariables struct {
// CommonVariables contains common variables. // CommonVariables contains common variables.
CommonVariables CommonVariables
// Region is the AWS region to use. // Region is the AWS region to use.
@ -61,8 +61,40 @@ type AWSVariables struct {
Debug bool Debug bool
} }
// GCPVariables is user configuration for creating a cluster with Terraform on GCP. func (v *AWSClusterVariables) String() string {
type GCPVariables struct { b := &strings.Builder{}
b.WriteString(v.CommonVariables.String())
writeLinef(b, "region = %q", v.Region)
writeLinef(b, "zone = %q", v.Zone)
writeLinef(b, "ami = %q", v.AMIImageID)
writeLinef(b, "instance_type = %q", v.InstanceType)
writeLinef(b, "state_disk_type = %q", v.StateDiskType)
writeLinef(b, "iam_instance_profile_control_plane = %q", v.IAMProfileControlPlane)
writeLinef(b, "iam_instance_profile_worker_nodes = %q", v.IAMProfileWorkerNodes)
writeLinef(b, "debug = %t", v.Debug)
return b.String()
}
// AWSIAMVariables is user configuration for creating the IAM configuration with Terraform on Microsoft Azure.
type AWSIAMVariables struct {
// Region is the AWS location to use. (e.g. us-east-2)
Region string
// Prefix is the name prefix of the resources to use.
Prefix string
}
// String returns a string representation of the IAM-specific variables, formatted as Terraform variables.
func (v *AWSIAMVariables) String() string {
b := &strings.Builder{}
writeLinef(b, "name_prefix = %q", v.Prefix)
writeLinef(b, "region = %q", v.Region)
return b.String()
}
// GCPClusterVariables is user configuration for creating resources with Terraform on GCP.
type GCPClusterVariables struct {
// CommonVariables contains common variables. // CommonVariables contains common variables.
CommonVariables CommonVariables
@ -84,23 +116,8 @@ type GCPVariables struct {
Debug bool Debug bool
} }
func (v *AWSVariables) String() string {
b := &strings.Builder{}
b.WriteString(v.CommonVariables.String())
writeLinef(b, "region = %q", v.Region)
writeLinef(b, "zone = %q", v.Zone)
writeLinef(b, "ami = %q", v.AMIImageID)
writeLinef(b, "instance_type = %q", v.InstanceType)
writeLinef(b, "state_disk_type = %q", v.StateDiskType)
writeLinef(b, "iam_instance_profile_control_plane = %q", v.IAMProfileControlPlane)
writeLinef(b, "iam_instance_profile_worker_nodes = %q", v.IAMProfileWorkerNodes)
writeLinef(b, "debug = %t", v.Debug)
return b.String()
}
// String returns a string representation of the variables, formatted as Terraform variables. // String returns a string representation of the variables, formatted as Terraform variables.
func (v *GCPVariables) String() string { func (v *GCPClusterVariables) String() string {
b := &strings.Builder{} b := &strings.Builder{}
b.WriteString(v.CommonVariables.String()) b.WriteString(v.CommonVariables.String())
writeLinef(b, "project = %q", v.Project) writeLinef(b, "project = %q", v.Project)
@ -114,8 +131,31 @@ func (v *GCPVariables) String() string {
return b.String() return b.String()
} }
// AzureVariables is user configuration for creating a cluster with Terraform on Azure. // GCPIAMVariables is user configuration for creating the IAM confioguration with Terraform on GCP.
type AzureVariables struct { type GCPIAMVariables struct {
// 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
// ServiceAccountID is the ID of the service account to use.
ServiceAccountID string
}
// String returns a string representation of the IAM-specific variables, formatted as Terraform variables.
func (v *GCPIAMVariables) String() string {
b := &strings.Builder{}
writeLinef(b, "project_id = %q", v.Project)
writeLinef(b, "region = %q", v.Region)
writeLinef(b, "zone = %q", v.Zone)
writeLinef(b, "service_account_id = %q", v.ServiceAccountID)
return b.String()
}
// AzureClusterVariables is user configuration for creating a cluster with Terraform on Azure.
type AzureClusterVariables struct {
// CommonVariables contains common variables. // CommonVariables contains common variables.
CommonVariables CommonVariables
@ -140,7 +180,7 @@ type AzureVariables struct {
} }
// String returns a string representation of the variables, formatted as Terraform variables. // String returns a string representation of the variables, formatted as Terraform variables.
func (v *AzureVariables) String() string { func (v *AzureClusterVariables) String() string {
b := &strings.Builder{} b := &strings.Builder{}
b.WriteString(v.CommonVariables.String()) b.WriteString(v.CommonVariables.String())
writeLinef(b, "resource_group = %q", v.ResourceGroup) writeLinef(b, "resource_group = %q", v.ResourceGroup)
@ -156,6 +196,26 @@ func (v *AzureVariables) String() string {
return b.String() return b.String()
} }
// AzureIAMVariables is user configuration for creating the IAM configuration with Terraform on Microsoft Azure.
type AzureIAMVariables struct {
// Region is the Azure region to use. (e.g. westus)
Region string
// ServicePrincipal is the name of the service principal to use.
ServicePrincipal string
// ResourceGroup is the name of the resource group to use.
ResourceGroup string
}
// String returns a string representation of the IAM-specific variables, formatted as Terraform variables.
func (v *AzureIAMVariables) String() string {
b := &strings.Builder{}
writeLinef(b, "service_principal_name = %q", v.ServicePrincipal)
writeLinef(b, "region = %q", v.Region)
writeLinef(b, "resource_group_name = %q", v.ResourceGroup)
return b.String()
}
// QEMUVariables is user configuration for creating a QEMU cluster with Terraform. // QEMUVariables is user configuration for creating a QEMU cluster with Terraform.
type QEMUVariables struct { type QEMUVariables struct {
// CommonVariables contains common variables. // CommonVariables contains common variables.

View File

@ -1,32 +0,0 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/google" {
version = "4.44.1"
constraints = "4.44.1"
hashes = [
"h1:BF4teLn5CRL7mU/bx+Xt2WkwYefVnvOszBgOKSXriAs=",
"h1:CscOj39+trGfPFHhB/GZbkF1RIRuksg6HZw61W+MuDE=",
"h1:Cx7FXPPBZ+As+EPVypmgjwo8PXXV7pLhnk/6JDzxnro=",
"h1:EH0DUD1ntp8TWOuQWQoi2NZbSN+5FFgEwH8DtffslEA=",
"h1:GJMP+l9nhw+G1J/l4gyRxlUXYOAbmyKl//u8dNWmCk0=",
"h1:a7Ju1uXrlWfOCKFerwUej9KReUp78JbawOtVrOkBMwM=",
"h1:fL4NeRoxY9WGGCpO+SqIkLPm4sFbPykpN2DuCMvR2q4=",
"h1:l3jJuHRgwD/tLKfifUvkIP6wQgEIpWSnhOaMIZnal7c=",
"h1:m1mVJKnt+s+FbfwqSm6/kcGy6FakWqPV8xjGqwNu+Gg=",
"h1:raHEXJpQCHohqYupGPt56hD7LrO0uq3O/ve7x4AQ14I=",
"h1:uU1/bSQgQAhtXIcMOU1O7kpQK/1uuVpN3UeGJYjG/z8=",
"zh:0668252985a677707bf46e393a96ea63b58b93d730f4daf8357246f7e6dd8ba9",
"zh:1cbfa5c7dcf02acb90718474a6b0e6af6a7c839c964270feaf55cddb537ef762",
"zh:44d601bc4667158c45ab584e60662f69d38e9febbb65b9bb1c5a84fdccc8b91e",
"zh:452a2c703c5c6024696d818a1e008067fa29cdf99bdae6847d5f8eed7a0b4d75",
"zh:55a0a22fd03fabdfff7debb5840ffa68a93850c864f48a852d6eb1f74ecae915",
"zh:7f59721375d9bb05fcc6eb17991a7bf1aab1b0f180107515ec3b9e298c6c6152",
"zh:a09ce6734c8f2cfb1b5855f073105b2403305fee0f68d846b1303ca37d516a28",
"zh:ae8e413ee02824c44b85b4d0aa71335940757a7a33922f55c13a800bc5e15b45",
"zh:c2947aea252929ba608bcc5b892d045541ce7224f104a542c18c5487287df9ef",
"zh:df463ad9ef19641e4502879af051dc17c6880882d436026255d8f9103992dc42",
"zh:ee113c1f6e32fa4c41fda9191d7fd50a1137d1072d1a39627fe90e10941d48ea",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
]
}

View File

@ -73,6 +73,10 @@ const (
MasterSecretFilename = "constellation-mastersecret.json" MasterSecretFilename = "constellation-mastersecret.json"
// TerraformWorkingDir is the directory name for the TerraformClient workspace. // TerraformWorkingDir is the directory name for the TerraformClient workspace.
TerraformWorkingDir = "constellation-terraform" TerraformWorkingDir = "constellation-terraform"
// TerraformIAMWorkingDir is the directory name for the Terraform IAM Client workspace.
TerraformIAMWorkingDir = "constellation-iam-terraform"
// GCPServiceAccountKeyFile is the file name for the GCP service account key file.
GCPServiceAccountKeyFile = "gcpServiceAccountKey.json"
// ControlPlaneAdminConfFilename filepath to control plane kubernetes admin config. // ControlPlaneAdminConfFilename filepath to control plane kubernetes admin config.
ControlPlaneAdminConfFilename = "/etc/kubernetes/admin.conf" ControlPlaneAdminConfFilename = "/etc/kubernetes/admin.conf"
// KubectlPath path to kubectl binary. // KubectlPath path to kubectl binary.