mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-25 15:39:37 -05:00
cli: iam destroy (#946)
This commit is contained in:
parent
f1b331bbbd
commit
5137e9fa57
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||||
"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"
|
||||||
|
tfjson "github.com/hashicorp/terraform-json"
|
||||||
)
|
)
|
||||||
|
|
||||||
type terraformClient interface {
|
type terraformClient interface {
|
||||||
@ -22,6 +23,7 @@ type terraformClient interface {
|
|||||||
Destroy(ctx context.Context) error
|
Destroy(ctx context.Context) error
|
||||||
CleanUpWorkspace() error
|
CleanUpWorkspace() error
|
||||||
RemoveInstaller()
|
RemoveInstaller()
|
||||||
|
Show(ctx context.Context) (*tfjson.State, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type libvirtRunner interface {
|
type libvirtRunner interface {
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||||
"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"
|
||||||
|
tfjson "github.com/hashicorp/terraform-json"
|
||||||
|
|
||||||
"go.uber.org/goleak"
|
"go.uber.org/goleak"
|
||||||
)
|
)
|
||||||
@ -30,14 +31,17 @@ type stubTerraformClient struct {
|
|||||||
initSecret string
|
initSecret string
|
||||||
iamOutput terraform.IAMOutput
|
iamOutput terraform.IAMOutput
|
||||||
uid string
|
uid string
|
||||||
|
tfjsonState *tfjson.State
|
||||||
cleanUpWorkspaceCalled bool
|
cleanUpWorkspaceCalled bool
|
||||||
removeInstallerCalled bool
|
removeInstallerCalled bool
|
||||||
destroyCalled bool
|
destroyCalled bool
|
||||||
|
showCalled bool
|
||||||
createClusterErr error
|
createClusterErr error
|
||||||
destroyErr error
|
destroyErr error
|
||||||
prepareWorkspaceErr error
|
prepareWorkspaceErr error
|
||||||
cleanUpWorkspaceErr error
|
cleanUpWorkspaceErr error
|
||||||
iamOutputErr error
|
iamOutputErr error
|
||||||
|
showErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *stubTerraformClient) CreateCluster(ctx context.Context) (terraform.CreateOutput, error) {
|
func (c *stubTerraformClient) CreateCluster(ctx context.Context) (terraform.CreateOutput, error) {
|
||||||
@ -70,6 +74,11 @@ func (c *stubTerraformClient) RemoveInstaller() {
|
|||||||
c.removeInstallerCalled = true
|
c.removeInstallerCalled = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *stubTerraformClient) Show(ctx context.Context) (*tfjson.State, error) {
|
||||||
|
c.showCalled = true
|
||||||
|
return c.tfjsonState, c.showErr
|
||||||
|
}
|
||||||
|
|
||||||
type stubLibvirtRunner struct {
|
type stubLibvirtRunner struct {
|
||||||
startCalled bool
|
startCalled bool
|
||||||
stopCalled bool
|
stopCalled bool
|
||||||
|
@ -7,6 +7,9 @@ package cloudcmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"path"
|
"path"
|
||||||
@ -15,9 +18,65 @@ import (
|
|||||||
"github.com/edgelesssys/constellation/v2/cli/internal/iamid"
|
"github.com/edgelesssys/constellation/v2/cli/internal/iamid"
|
||||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/constants"
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// IAMDestroyer destroys an IAM configuration.
|
||||||
|
type IAMDestroyer struct {
|
||||||
|
client terraformClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIAMDestroyer creates a new IAM Destroyer.
|
||||||
|
func NewIAMDestroyer(ctx context.Context) (*IAMDestroyer, error) {
|
||||||
|
cl, err := terraform.New(ctx, constants.TerraformIAMWorkingDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &IAMDestroyer{client: cl}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTfstateServiceAccountKey returns the sa_key output from the terraform state.
|
||||||
|
func (d *IAMDestroyer) GetTfstateServiceAccountKey(ctx context.Context) (gcpshared.ServiceAccountKey, error) {
|
||||||
|
tfState, err := d.client.Show(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return gcpshared.ServiceAccountKey{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tfState.Values == nil {
|
||||||
|
return gcpshared.ServiceAccountKey{}, errors.New("no Values field in terraform state")
|
||||||
|
}
|
||||||
|
|
||||||
|
saKeyJSON := tfState.Values.Outputs["sa_key"]
|
||||||
|
if saKeyJSON == nil {
|
||||||
|
return gcpshared.ServiceAccountKey{}, errors.New("no sa_key in outputs of the terraform state")
|
||||||
|
}
|
||||||
|
|
||||||
|
saKeyString, ok := saKeyJSON.Value.(string)
|
||||||
|
if !ok {
|
||||||
|
return gcpshared.ServiceAccountKey{}, errors.New("sa_key field in terraform state is not a string")
|
||||||
|
}
|
||||||
|
saKey, err := base64.StdEncoding.DecodeString(saKeyString)
|
||||||
|
if err != nil {
|
||||||
|
return gcpshared.ServiceAccountKey{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var tfSaKey gcpshared.ServiceAccountKey
|
||||||
|
if err := json.Unmarshal(saKey, &tfSaKey); err != nil {
|
||||||
|
return gcpshared.ServiceAccountKey{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tfSaKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DestroyIAMConfiguration destroys the previously created IAM configuration and deletes the local IAM terraform files.
|
||||||
|
func (d *IAMDestroyer) DestroyIAMConfiguration(ctx context.Context) error {
|
||||||
|
if err := d.client.Destroy(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return d.client.CleanUpWorkspace()
|
||||||
|
}
|
||||||
|
|
||||||
// IAMCreator creates the IAM configuration on the cloud provider.
|
// IAMCreator creates the IAM configuration on the cloud provider.
|
||||||
type IAMCreator struct {
|
type IAMCreator struct {
|
||||||
out io.Writer
|
out io.Writer
|
||||||
|
@ -8,13 +8,18 @@ package cloudcmd
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/edgelesssys/constellation/v2/cli/internal/iamid"
|
"github.com/edgelesssys/constellation/v2/cli/internal/iamid"
|
||||||
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
"github.com/edgelesssys/constellation/v2/cli/internal/terraform"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared"
|
||||||
|
tfjson "github.com/hashicorp/terraform-json"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIAMCreator(t *testing.T) {
|
func TestIAMCreator(t *testing.T) {
|
||||||
@ -148,3 +153,186 @@ func TestIAMCreator(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDestroyIAMConfiguration(t *testing.T) {
|
||||||
|
newError := func() error {
|
||||||
|
return errors.New("failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
tfClient *stubTerraformClient
|
||||||
|
wantErr bool
|
||||||
|
wantDestroyCalled bool
|
||||||
|
wantCleanupWorkspaceCalled bool
|
||||||
|
}{
|
||||||
|
"destroy error": {
|
||||||
|
tfClient: &stubTerraformClient{destroyErr: newError()},
|
||||||
|
wantErr: true,
|
||||||
|
wantDestroyCalled: true,
|
||||||
|
},
|
||||||
|
"destroy": {
|
||||||
|
tfClient: &stubTerraformClient{},
|
||||||
|
wantDestroyCalled: true,
|
||||||
|
wantCleanupWorkspaceCalled: true,
|
||||||
|
},
|
||||||
|
"cleanup error": {
|
||||||
|
tfClient: &stubTerraformClient{cleanUpWorkspaceErr: newError()},
|
||||||
|
wantErr: true,
|
||||||
|
wantDestroyCalled: true,
|
||||||
|
wantCleanupWorkspaceCalled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
destroyer := &IAMDestroyer{client: tc.tfClient}
|
||||||
|
|
||||||
|
err := destroyer.DestroyIAMConfiguration(context.Background())
|
||||||
|
|
||||||
|
if tc.wantErr {
|
||||||
|
assert.Error(err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(tc.wantDestroyCalled, tc.tfClient.destroyCalled)
|
||||||
|
assert.Equal(tc.wantCleanupWorkspaceCalled, tc.tfClient.cleanUpWorkspaceCalled)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetTfstateServiceAccountKey(t *testing.T) {
|
||||||
|
someError := errors.New("failed")
|
||||||
|
|
||||||
|
gcpFile := `
|
||||||
|
{
|
||||||
|
"auth_provider_x509_cert_url": "",
|
||||||
|
"auth_uri": "",
|
||||||
|
"client_email": "",
|
||||||
|
"client_id": "",
|
||||||
|
"client_x509_cert_url": "",
|
||||||
|
"private_key": "",
|
||||||
|
"private_key_id": "",
|
||||||
|
"project_id": "",
|
||||||
|
"token_uri": "",
|
||||||
|
"type": ""
|
||||||
|
}
|
||||||
|
`
|
||||||
|
gcpFileB64 := base64.StdEncoding.EncodeToString([]byte(gcpFile))
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
cl *stubTerraformClient
|
||||||
|
wantValidSaKey bool
|
||||||
|
wantErr bool
|
||||||
|
wantShowCalled bool
|
||||||
|
}{
|
||||||
|
"valid": {
|
||||||
|
cl: &stubTerraformClient{
|
||||||
|
tfjsonState: &tfjson.State{
|
||||||
|
Values: &tfjson.StateValues{
|
||||||
|
Outputs: map[string]*tfjson.StateOutput{
|
||||||
|
"sa_key": {
|
||||||
|
Value: gcpFileB64,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantValidSaKey: true,
|
||||||
|
wantShowCalled: true,
|
||||||
|
},
|
||||||
|
"show error": {
|
||||||
|
cl: &stubTerraformClient{
|
||||||
|
showErr: someError,
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantShowCalled: true,
|
||||||
|
},
|
||||||
|
"nil tfstate values": {
|
||||||
|
cl: &stubTerraformClient{
|
||||||
|
tfjsonState: &tfjson.State{
|
||||||
|
Values: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantShowCalled: true,
|
||||||
|
},
|
||||||
|
"no key": {
|
||||||
|
cl: &stubTerraformClient{
|
||||||
|
tfjsonState: &tfjson.State{
|
||||||
|
Values: &tfjson.StateValues{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantShowCalled: true,
|
||||||
|
},
|
||||||
|
"invalid base64": {
|
||||||
|
cl: &stubTerraformClient{
|
||||||
|
tfjsonState: &tfjson.State{
|
||||||
|
Values: &tfjson.StateValues{
|
||||||
|
Outputs: map[string]*tfjson.StateOutput{
|
||||||
|
"sa_key": {
|
||||||
|
Value: "iamnotvalid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantShowCalled: true,
|
||||||
|
},
|
||||||
|
"valid base64 invalid json": {
|
||||||
|
cl: &stubTerraformClient{
|
||||||
|
tfjsonState: &tfjson.State{
|
||||||
|
Values: &tfjson.StateValues{
|
||||||
|
Outputs: map[string]*tfjson.StateOutput{
|
||||||
|
"sa_key": {
|
||||||
|
Value: base64.StdEncoding.EncodeToString([]byte("asdf")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantShowCalled: true,
|
||||||
|
},
|
||||||
|
"not string": {
|
||||||
|
cl: &stubTerraformClient{
|
||||||
|
tfjsonState: &tfjson.State{
|
||||||
|
Values: &tfjson.StateValues{
|
||||||
|
Outputs: map[string]*tfjson.StateOutput{
|
||||||
|
"sa_key": {
|
||||||
|
Value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
wantShowCalled: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
destroyer := IAMDestroyer{client: tc.cl}
|
||||||
|
|
||||||
|
saKey, err := destroyer.GetTfstateServiceAccountKey(context.Background())
|
||||||
|
|
||||||
|
if tc.wantErr {
|
||||||
|
assert.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(err)
|
||||||
|
|
||||||
|
var saKeyComp gcpshared.ServiceAccountKey
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(gcpFile), &saKeyComp))
|
||||||
|
|
||||||
|
assert.Equal(saKey, saKeyComp)
|
||||||
|
assert.Equal(tc.wantShowCalled, tc.cl.showCalled)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -13,6 +13,7 @@ import (
|
|||||||
"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/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/cloud/gcpshared"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -34,6 +35,11 @@ type cloudIAMCreator interface {
|
|||||||
) (iamid.File, error)
|
) (iamid.File, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type iamDestroyer interface {
|
||||||
|
DestroyIAMConfiguration(ctx context.Context) error
|
||||||
|
GetTfstateServiceAccountKey(ctx context.Context) (gcpshared.ServiceAccountKey, error)
|
||||||
|
}
|
||||||
|
|
||||||
type cloudTerminator interface {
|
type cloudTerminator interface {
|
||||||
Terminate(context.Context) error
|
Terminate(context.Context) error
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"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/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/cloud/gcpshared"
|
||||||
"github.com/edgelesssys/constellation/v2/internal/config"
|
"github.com/edgelesssys/constellation/v2/internal/config"
|
||||||
"go.uber.org/goleak"
|
"go.uber.org/goleak"
|
||||||
)
|
)
|
||||||
@ -72,3 +73,21 @@ func (c *stubIAMCreator) Create(
|
|||||||
c.id.CloudProvider = provider
|
c.id.CloudProvider = provider
|
||||||
return c.id, c.createErr
|
return c.id, c.createErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type stubIAMDestroyer struct {
|
||||||
|
destroyCalled bool
|
||||||
|
getTfstateKeyCalled bool
|
||||||
|
gcpSaKey gcpshared.ServiceAccountKey
|
||||||
|
destroyErr error
|
||||||
|
getTfstateKeyErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *stubIAMDestroyer) DestroyIAMConfiguration(ctx context.Context) error {
|
||||||
|
d.destroyCalled = true
|
||||||
|
return d.destroyErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *stubIAMDestroyer) GetTfstateServiceAccountKey(ctx context.Context) (gcpshared.ServiceAccountKey, error) {
|
||||||
|
d.getTfstateKeyCalled = true
|
||||||
|
return d.gcpSaKey, d.getTfstateKeyErr
|
||||||
|
}
|
||||||
|
@ -42,6 +42,7 @@ func NewIAMCmd() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd.AddCommand(newIAMCreateCmd())
|
cmd.AddCommand(newIAMCreateCmd())
|
||||||
|
cmd.AddCommand(newIAMDestroyCmd())
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
@ -563,3 +564,18 @@ func parseIDFile(serviceAccountKeyBase64 string) (map[string]string, error) {
|
|||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewIAMDestroyCmd returns a new cobra.Command for the iam destroy subcommand.
|
||||||
|
func newIAMDestroyCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "destroy",
|
||||||
|
Short: "Destroy an IAM configuration and delete local terraform files",
|
||||||
|
Long: "Destroy an IAM configuration and delete local terraform files.",
|
||||||
|
Args: cobra.ExactArgs(0),
|
||||||
|
RunE: runIAMDestroy,
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Flags().BoolP("yes", "y", false, "destroy the IAM configuration without asking for confirmation")
|
||||||
|
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
142
cli/internal/cmd/iamdestroy.go
Normal file
142
cli/internal/cmd/iamdestroy.go
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runIAMDestroy(cmd *cobra.Command, _args []string) error {
|
||||||
|
log, err := newCLILogger(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating logger: %w", err)
|
||||||
|
}
|
||||||
|
defer log.Sync()
|
||||||
|
spinner := newSpinner(cmd.ErrOrStderr())
|
||||||
|
destroyer, err := cloudcmd.NewIAMDestroyer(cmd.Context())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fsHandler := file.NewHandler(afero.NewOsFs())
|
||||||
|
|
||||||
|
c := &destroyCmd{log: log}
|
||||||
|
|
||||||
|
return c.iamDestroy(cmd, spinner, destroyer, fsHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
type destroyCmd struct {
|
||||||
|
log debugLog
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *destroyCmd) iamDestroy(cmd *cobra.Command, spinner spinnerInterf, destroyer iamDestroyer, fsHandler file.Handler) error {
|
||||||
|
// check if there is a possibility that the cluster is still running by looking out for specific files
|
||||||
|
c.log.Debugf("Checking if %q exists", constants.AdminConfFilename)
|
||||||
|
_, err := fsHandler.Stat(constants.AdminConfFilename)
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return fmt.Errorf("file %q still exists, please make sure to terminate your cluster before destroying your IAM configuration", constants.AdminConfFilename)
|
||||||
|
}
|
||||||
|
c.log.Debugf("Checking if %q exists", constants.ClusterIDsFileName)
|
||||||
|
_, err = fsHandler.Stat(constants.ClusterIDsFileName)
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return fmt.Errorf("file %q still exists, please make sure to terminate your cluster before destroying your IAM configuration", constants.ClusterIDsFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
yes, err := cmd.Flags().GetBool("yes")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.log.Debugf("\"yes\" flag is set to %t", yes)
|
||||||
|
|
||||||
|
gcpFileExists := false
|
||||||
|
|
||||||
|
c.log.Debugf("Checking if %q exists", constants.GCPServiceAccountKeyFile)
|
||||||
|
_, err = fsHandler.Stat(constants.GCPServiceAccountKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.log.Debugf("%q exists", constants.GCPServiceAccountKeyFile)
|
||||||
|
gcpFileExists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !yes {
|
||||||
|
// Confirmation
|
||||||
|
confirmString := "Do you really want to destroy your IAM configuration?"
|
||||||
|
if gcpFileExists {
|
||||||
|
confirmString += fmt.Sprintf(" (This will also delete %q)", constants.GCPServiceAccountKeyFile)
|
||||||
|
}
|
||||||
|
ok, err := askToConfirm(cmd, confirmString)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
cmd.Println("The destruction of the IAM configuration was aborted")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if gcpFileExists {
|
||||||
|
c.log.Debugf("Starting to delete %q", constants.GCPServiceAccountKeyFile)
|
||||||
|
proceed, err := c.deleteGCPServiceAccountKeyFile(cmd, destroyer, fsHandler)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !proceed {
|
||||||
|
cmd.Println("Destruction was aborted")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.log.Debugf("Starting to destroy IAM configuration")
|
||||||
|
|
||||||
|
spinner.Start("Destroying IAM configuration", false)
|
||||||
|
defer spinner.Stop()
|
||||||
|
if err := destroyer.DestroyIAMConfiguration(cmd.Context()); err != nil {
|
||||||
|
return fmt.Errorf("destroying IAM configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner.Stop() // stop the spinner to print a new line
|
||||||
|
fmt.Println("Successfully destroyed IAM configuration")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *destroyCmd) deleteGCPServiceAccountKeyFile(cmd *cobra.Command, destroyer iamDestroyer, fsHandler file.Handler) (bool, error) {
|
||||||
|
var fileSaKey gcpshared.ServiceAccountKey
|
||||||
|
|
||||||
|
c.log.Debugf("Parsing %q", constants.GCPServiceAccountKeyFile)
|
||||||
|
if err := fsHandler.ReadJSON(constants.GCPServiceAccountKeyFile, &fileSaKey); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.log.Debugf("Getting service account key from the tfstate")
|
||||||
|
tfSaKey, err := destroyer.GetTfstateServiceAccountKey(cmd.Context())
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.log.Debugf("Checking if keys are the same")
|
||||||
|
if tfSaKey != fileSaKey {
|
||||||
|
cmd.Printf("The key in %q don't match up with your terraform state. %q will not be deleted.\n", constants.GCPServiceAccountKeyFile, constants.GCPServiceAccountKeyFile)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := fsHandler.Remove(constants.GCPServiceAccountKeyFile); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.log.Debugf("Successfully deleted %q", constants.GCPServiceAccountKeyFile)
|
||||||
|
return true, nil
|
||||||
|
}
|
211
cli/internal/cmd/iamdestroy_test.go
Normal file
211
cli/internal/cmd/iamdestroy_test.go
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
Copyright (c) Edgeless Systems GmbH
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/constants"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/file"
|
||||||
|
"github.com/edgelesssys/constellation/v2/internal/logger"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIAMDestroy(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
someError := errors.New("failed")
|
||||||
|
|
||||||
|
newFsExists := func() file.Handler {
|
||||||
|
fh := file.NewHandler(afero.NewMemMapFs())
|
||||||
|
require.NoError(fh.Write(constants.GCPServiceAccountKeyFile, []byte("{}")))
|
||||||
|
return fh
|
||||||
|
}
|
||||||
|
newFsMissing := func() file.Handler {
|
||||||
|
fh := file.NewHandler(afero.NewMemMapFs())
|
||||||
|
return fh
|
||||||
|
}
|
||||||
|
newFsWithAdminConf := func() file.Handler {
|
||||||
|
fh := file.NewHandler(afero.NewMemMapFs())
|
||||||
|
require.NoError(fh.Write(constants.AdminConfFilename, []byte("")))
|
||||||
|
return fh
|
||||||
|
}
|
||||||
|
newFsWithClusterIDFile := func() file.Handler {
|
||||||
|
fh := file.NewHandler(afero.NewMemMapFs())
|
||||||
|
require.NoError(fh.Write(constants.ClusterIDsFileName, []byte("")))
|
||||||
|
return fh
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
iamDestroyer *stubIAMDestroyer
|
||||||
|
fh file.Handler
|
||||||
|
stdin string
|
||||||
|
yesFlag string
|
||||||
|
wantErr bool
|
||||||
|
wantDestroyCalled bool
|
||||||
|
}{
|
||||||
|
"cluster running admin conf": {
|
||||||
|
fh: newFsWithAdminConf(),
|
||||||
|
iamDestroyer: &stubIAMDestroyer{},
|
||||||
|
yesFlag: "false",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"cluster running cluster ids": {
|
||||||
|
fh: newFsWithClusterIDFile(),
|
||||||
|
iamDestroyer: &stubIAMDestroyer{},
|
||||||
|
yesFlag: "false",
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"file missing abort": {
|
||||||
|
fh: newFsMissing(),
|
||||||
|
stdin: "n\n",
|
||||||
|
yesFlag: "false",
|
||||||
|
iamDestroyer: &stubIAMDestroyer{},
|
||||||
|
},
|
||||||
|
"file missing": {
|
||||||
|
fh: newFsMissing(),
|
||||||
|
stdin: "y\n",
|
||||||
|
yesFlag: "false",
|
||||||
|
iamDestroyer: &stubIAMDestroyer{},
|
||||||
|
wantDestroyCalled: true,
|
||||||
|
},
|
||||||
|
"file exists abort": {
|
||||||
|
fh: newFsExists(),
|
||||||
|
stdin: "n\n",
|
||||||
|
yesFlag: "false",
|
||||||
|
iamDestroyer: &stubIAMDestroyer{},
|
||||||
|
},
|
||||||
|
"error destroying user": {
|
||||||
|
fh: newFsMissing(),
|
||||||
|
stdin: "y\n",
|
||||||
|
yesFlag: "false",
|
||||||
|
iamDestroyer: &stubIAMDestroyer{destroyErr: someError},
|
||||||
|
wantErr: true,
|
||||||
|
wantDestroyCalled: true,
|
||||||
|
},
|
||||||
|
"gcp delete error": {
|
||||||
|
fh: newFsExists(),
|
||||||
|
yesFlag: "true",
|
||||||
|
iamDestroyer: &stubIAMDestroyer{getTfstateKeyErr: someError},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
cmd := newIAMDestroyCmd()
|
||||||
|
cmd.SetOut(&bytes.Buffer{})
|
||||||
|
cmd.SetErr(&bytes.Buffer{})
|
||||||
|
cmd.SetIn(bytes.NewBufferString(tc.stdin))
|
||||||
|
assert.NoError(cmd.Flags().Set("yes", tc.yesFlag))
|
||||||
|
|
||||||
|
c := &destroyCmd{log: logger.NewTest(t)}
|
||||||
|
|
||||||
|
err := c.iamDestroy(cmd, &nopSpinner{}, tc.iamDestroyer, tc.fh)
|
||||||
|
|
||||||
|
if tc.wantErr {
|
||||||
|
assert.Error(err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(err)
|
||||||
|
}
|
||||||
|
assert.Equal(tc.wantDestroyCalled, tc.iamDestroyer.destroyCalled)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteGCPServiceAccountKeyFile(t *testing.T) {
|
||||||
|
require := require.New(t)
|
||||||
|
someError := errors.New("failed")
|
||||||
|
|
||||||
|
gcpFile := `
|
||||||
|
{
|
||||||
|
"auth_provider_x509_cert_url": "",
|
||||||
|
"auth_uri": "",
|
||||||
|
"client_email": "",
|
||||||
|
"client_id": "",
|
||||||
|
"client_x509_cert_url": "",
|
||||||
|
"private_key": "",
|
||||||
|
"private_key_id": "",
|
||||||
|
"project_id": "",
|
||||||
|
"token_uri": "",
|
||||||
|
"type": ""
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
newFs := func() file.Handler {
|
||||||
|
fs := file.NewHandler(afero.NewMemMapFs())
|
||||||
|
require.NoError(fs.Write(constants.GCPServiceAccountKeyFile, []byte(gcpFile)))
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
newFsInvalidJSON := func() file.Handler {
|
||||||
|
fh := file.NewHandler(afero.NewMemMapFs())
|
||||||
|
require.NoError(fh.Write(constants.GCPServiceAccountKeyFile, []byte("asdf")))
|
||||||
|
return fh
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
destroyer *stubIAMDestroyer
|
||||||
|
fsHandler file.Handler
|
||||||
|
stdin string
|
||||||
|
wantErr bool
|
||||||
|
wantProceed bool
|
||||||
|
wantGetSaKeyCalled bool
|
||||||
|
}{
|
||||||
|
"invalid gcp json": {
|
||||||
|
destroyer: &stubIAMDestroyer{},
|
||||||
|
fsHandler: newFsInvalidJSON(),
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
"error getting key terraform": {
|
||||||
|
destroyer: &stubIAMDestroyer{getTfstateKeyErr: someError},
|
||||||
|
fsHandler: newFs(),
|
||||||
|
wantErr: true,
|
||||||
|
wantGetSaKeyCalled: true,
|
||||||
|
},
|
||||||
|
"keys not same": {
|
||||||
|
destroyer: &stubIAMDestroyer{gcpSaKey: gcpshared.ServiceAccountKey{
|
||||||
|
Type: "somethingelse",
|
||||||
|
}},
|
||||||
|
fsHandler: newFs(),
|
||||||
|
wantGetSaKeyCalled: true,
|
||||||
|
wantProceed: true,
|
||||||
|
},
|
||||||
|
"valid": {
|
||||||
|
destroyer: &stubIAMDestroyer{},
|
||||||
|
fsHandler: newFs(),
|
||||||
|
wantGetSaKeyCalled: true,
|
||||||
|
wantProceed: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range testCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
|
||||||
|
cmd := newIAMDestroyCmd()
|
||||||
|
cmd.SetOut(&bytes.Buffer{})
|
||||||
|
cmd.SetErr(&bytes.Buffer{})
|
||||||
|
cmd.SetIn(bytes.NewBufferString(tc.stdin))
|
||||||
|
|
||||||
|
c := &destroyCmd{log: logger.NewTest(t)}
|
||||||
|
|
||||||
|
proceed, err := c.deleteGCPServiceAccountKeyFile(cmd, tc.destroyer, tc.fsHandler)
|
||||||
|
if tc.wantErr {
|
||||||
|
assert.Error(err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(tc.wantProceed, proceed)
|
||||||
|
assert.Equal(tc.wantGetSaKeyCalled, tc.destroyer.getTfstateKeyCalled)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -68,6 +68,11 @@ func New(ctx context.Context, workingDir string) (*Client, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show reads the default state path and outputs the state.
|
||||||
|
func (c *Client) Show(ctx context.Context) (*tfjson.State, error) {
|
||||||
|
return c.tf.Show(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
// PrepareWorkspace prepares a Terraform workspace for a Constellation cluster.
|
// PrepareWorkspace prepares a Terraform workspace for a Constellation cluster.
|
||||||
func (c *Client) PrepareWorkspace(path string, vars Variables) error {
|
func (c *Client) PrepareWorkspace(path string, vars Variables) error {
|
||||||
if err := prepareWorkspace(path, c.file, c.workingDir); err != nil {
|
if err := prepareWorkspace(path, c.file, c.workingDir); err != nil {
|
||||||
|
@ -256,14 +256,7 @@ You can keep created IAM configurations and reuse them for new clusters. Alterna
|
|||||||
* [Terraform](https://developer.hashicorp.com/terraform/downloads) is installed on your machine.
|
* [Terraform](https://developer.hashicorp.com/terraform/downloads) is installed on your machine.
|
||||||
* Access to the `terraform.tfstate` file created by the `constellation iam create` command.
|
* Access to the `terraform.tfstate` file created by the `constellation iam create` command.
|
||||||
|
|
||||||
You can delete the IAM configuration using the following commands:
|
You can delete the IAM configuration by executing the following command in the same directory where you executed `constellation iam create`:
|
||||||
```bash
|
```bash
|
||||||
# Navigate to the directory containing the terraform.tfstate file
|
constellation iam destroy
|
||||||
cd constellation-iam-terraform
|
|
||||||
# Destroy the IAM configuration via Terraform
|
|
||||||
terraform destroy
|
|
||||||
# Confirm deletion by typing "yes"
|
|
||||||
# Remove the Terraform state directory
|
|
||||||
cd ..
|
|
||||||
rm -rf constellation-iam-terraform
|
|
||||||
```
|
```
|
||||||
|
Loading…
Reference in New Issue
Block a user