From 1d0ee796e87ee7db3dc8ec8a4e5ccfc60c6688b3 Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Fri, 14 Apr 2023 14:15:07 +0200 Subject: [PATCH] cli: add Terraform log support (#1620) * add Terraform logging * add TF logging to CLI * fix path * only create file if logging is enabled * update bazel files * register persistent flags manually * clidocgen * move logging code to separate file * reword yes flag parsing error * update bazel buildfile * factor out log level setting --- .gitignore | 1 + cli/cmd/root.go | 1 + cli/internal/cloudcmd/clients.go | 6 +- cli/internal/cloudcmd/clients_test.go | 6 +- cli/internal/cloudcmd/create.go | 211 ++++++++++++----------- cli/internal/cloudcmd/create_test.go | 10 +- cli/internal/cloudcmd/iam.go | 59 +++---- cli/internal/cloudcmd/iam_test.go | 10 +- cli/internal/cloudcmd/rollback.go | 16 +- cli/internal/cloudcmd/rollback_test.go | 5 +- cli/internal/cloudcmd/terminate.go | 8 +- cli/internal/cloudcmd/terminate_test.go | 3 +- cli/internal/cmd/BUILD.bazel | 1 + cli/internal/cmd/cloud.go | 13 +- cli/internal/cmd/cloud_test.go | 15 +- cli/internal/cmd/create.go | 24 ++- cli/internal/cmd/create_test.go | 1 + cli/internal/cmd/iamcreate.go | 31 +++- cli/internal/cmd/iamcreate_test.go | 11 +- cli/internal/cmd/iamdestroy.go | 47 ++++- cli/internal/cmd/iamdestroy_test.go | 4 + cli/internal/cmd/miniup.go | 78 +++++++-- cli/internal/cmd/terminate.go | 34 +++- cli/internal/cmd/terminate_test.go | 3 + cli/internal/terraform/BUILD.bazel | 2 + cli/internal/terraform/logging.go | 75 ++++++++ cli/internal/terraform/terraform.go | 43 ++++- cli/internal/terraform/terraform_test.go | 178 +++++++++++++++++-- docs/docs/reference/cli.md | 28 ++- internal/constants/constants.go | 2 + 30 files changed, 688 insertions(+), 238 deletions(-) create mode 100644 cli/internal/terraform/logging.go diff --git a/.gitignore b/.gitignore index 47f139e5b..6db029bba 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ image/config.mk .terraform .terraform.tfstate.lock.info *.tfvars +terraform.log # macOS .DS_Store diff --git a/cli/cmd/root.go b/cli/cmd/root.go index e916738b8..c09d3689b 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -48,6 +48,7 @@ func NewRootCmd() *cobra.Command { rootCmd.PersistentFlags().Bool("debug", false, "enable debug logging") rootCmd.PersistentFlags().Bool("force", false, "disable version compatibility checks - might result in corrupted clusters") + rootCmd.PersistentFlags().String("tf-log", "NONE", "sets the Terraform log level (default \"NONE\" - no logs)") rootCmd.AddCommand(cmd.NewConfigCmd()) rootCmd.AddCommand(cmd.NewCreateCmd()) diff --git a/cli/internal/cloudcmd/clients.go b/cli/internal/cloudcmd/clients.go index 0428039ed..897be8ea8 100644 --- a/cli/internal/cloudcmd/clients.go +++ b/cli/internal/cloudcmd/clients.go @@ -23,9 +23,9 @@ type imageFetcher interface { type terraformClient interface { PrepareWorkspace(path string, input terraform.Variables) error - CreateCluster(ctx context.Context) (terraform.CreateOutput, error) - CreateIAMConfig(ctx context.Context, provider cloudprovider.Provider) (terraform.IAMOutput, error) - Destroy(ctx context.Context) error + CreateCluster(ctx context.Context, logLevel terraform.LogLevel) (terraform.CreateOutput, error) + CreateIAMConfig(ctx context.Context, provider cloudprovider.Provider, logLevel terraform.LogLevel) (terraform.IAMOutput, error) + Destroy(ctx context.Context, logLevel terraform.LogLevel) error CleanUpWorkspace() error RemoveInstaller() Show(ctx context.Context) (*tfjson.State, error) diff --git a/cli/internal/cloudcmd/clients_test.go b/cli/internal/cloudcmd/clients_test.go index f23aef737..750362a4f 100644 --- a/cli/internal/cloudcmd/clients_test.go +++ b/cli/internal/cloudcmd/clients_test.go @@ -45,7 +45,7 @@ type stubTerraformClient struct { showErr error } -func (c *stubTerraformClient) CreateCluster(_ context.Context) (terraform.CreateOutput, error) { +func (c *stubTerraformClient) CreateCluster(_ context.Context, _ terraform.LogLevel) (terraform.CreateOutput, error) { return terraform.CreateOutput{ IP: c.ip, Secret: c.initSecret, @@ -54,7 +54,7 @@ func (c *stubTerraformClient) CreateCluster(_ context.Context) (terraform.Create }, c.createClusterErr } -func (c *stubTerraformClient) CreateIAMConfig(_ context.Context, _ cloudprovider.Provider) (terraform.IAMOutput, error) { +func (c *stubTerraformClient) CreateIAMConfig(_ context.Context, _ cloudprovider.Provider, _ terraform.LogLevel) (terraform.IAMOutput, error) { return c.iamOutput, c.iamOutputErr } @@ -62,7 +62,7 @@ func (c *stubTerraformClient) PrepareWorkspace(_ string, _ terraform.Variables) return c.prepareWorkspaceErr } -func (c *stubTerraformClient) Destroy(_ context.Context) error { +func (c *stubTerraformClient) Destroy(_ context.Context, _ terraform.LogLevel) error { c.destroyCalled = true return c.destroyErr } diff --git a/cli/internal/cloudcmd/create.go b/cli/internal/cloudcmd/create.go index 4c8162018..6f1230fa6 100644 --- a/cli/internal/cloudcmd/create.go +++ b/cli/internal/cloudcmd/create.go @@ -63,43 +63,54 @@ func NewCreator(out io.Writer) *Creator { } } +// CreateOptions are the options for creating a Constellation cluster. +type CreateOptions struct { + Provider cloudprovider.Provider + Config *config.Config + InsType string + ControlPlaneCount int + WorkerCount int + image string + TFLogLevel terraform.LogLevel +} + // Create creates the handed amount of instances and all the needed resources. -func (c *Creator) Create(ctx context.Context, provider cloudprovider.Provider, config *config.Config, insType string, controlPlaneCount, workerCount int, -) (clusterid.File, error) { - image, err := c.image.FetchReference(ctx, config) +func (c *Creator) Create(ctx context.Context, opts CreateOptions) (clusterid.File, error) { + image, err := c.image.FetchReference(ctx, opts.Config) if err != nil { return clusterid.File{}, fmt.Errorf("fetching image reference: %w", err) } + opts.image = image - switch provider { + switch opts.Provider { case cloudprovider.AWS: cl, err := c.newTerraformClient(ctx) if err != nil { return clusterid.File{}, err } defer cl.RemoveInstaller() - return c.createAWS(ctx, cl, config, insType, controlPlaneCount, workerCount, image) + return c.createAWS(ctx, cl, opts) case cloudprovider.GCP: cl, err := c.newTerraformClient(ctx) if err != nil { return clusterid.File{}, err } defer cl.RemoveInstaller() - return c.createGCP(ctx, cl, config, insType, controlPlaneCount, workerCount, image) + return c.createGCP(ctx, cl, opts) case cloudprovider.Azure: cl, err := c.newTerraformClient(ctx) if err != nil { return clusterid.File{}, err } defer cl.RemoveInstaller() - return c.createAzure(ctx, cl, config, insType, controlPlaneCount, workerCount, image) + return c.createAzure(ctx, cl, opts) case cloudprovider.OpenStack: cl, err := c.newTerraformClient(ctx) if err != nil { return clusterid.File{}, err } defer cl.RemoveInstaller() - return c.createOpenStack(ctx, cl, config, controlPlaneCount, workerCount, image) + return c.createOpenStack(ctx, cl, opts) case cloudprovider.QEMU: if runtime.GOARCH != "amd64" || runtime.GOOS != "linux" { return clusterid.File{}, fmt.Errorf("creation of a QEMU based Constellation is not supported for %s/%s", runtime.GOOS, runtime.GOARCH) @@ -110,38 +121,40 @@ func (c *Creator) Create(ctx context.Context, provider cloudprovider.Provider, c } defer cl.RemoveInstaller() lv := c.newLibvirtRunner() - return c.createQEMU(ctx, cl, lv, config, controlPlaneCount, workerCount, image) + qemuOpts := qemuCreateOptions{ + source: image, + CreateOptions: opts, + } + return c.createQEMU(ctx, cl, lv, qemuOpts) default: - return clusterid.File{}, fmt.Errorf("unsupported cloud provider: %s", provider) + return clusterid.File{}, fmt.Errorf("unsupported cloud provider: %s", opts.Provider) } } -func (c *Creator) createAWS(ctx context.Context, cl terraformClient, config *config.Config, - insType string, controlPlaneCount, workerCount int, image string, -) (idFile clusterid.File, retErr error) { +func (c *Creator) createAWS(ctx context.Context, cl terraformClient, opts CreateOptions) (idFile clusterid.File, retErr error) { vars := terraform.AWSClusterVariables{ CommonVariables: terraform.CommonVariables{ - Name: config.Name, - CountControlPlanes: controlPlaneCount, - CountWorkers: workerCount, - StateDiskSizeGB: config.StateDiskSizeGB, + Name: opts.Config.Name, + CountControlPlanes: opts.ControlPlaneCount, + CountWorkers: opts.WorkerCount, + StateDiskSizeGB: opts.Config.StateDiskSizeGB, }, - StateDiskType: config.Provider.AWS.StateDiskType, - Region: config.Provider.AWS.Region, - Zone: config.Provider.AWS.Zone, - InstanceType: insType, - AMIImageID: image, - IAMProfileControlPlane: config.Provider.AWS.IAMProfileControlPlane, - IAMProfileWorkerNodes: config.Provider.AWS.IAMProfileWorkerNodes, - Debug: config.IsDebugCluster(), + StateDiskType: opts.Config.Provider.AWS.StateDiskType, + Region: opts.Config.Provider.AWS.Region, + Zone: opts.Config.Provider.AWS.Zone, + InstanceType: opts.InsType, + AMIImageID: opts.image, + IAMProfileControlPlane: opts.Config.Provider.AWS.IAMProfileControlPlane, + IAMProfileWorkerNodes: opts.Config.Provider.AWS.IAMProfileWorkerNodes, + Debug: opts.Config.IsDebugCluster(), } if err := cl.PrepareWorkspace(path.Join("terraform", strings.ToLower(cloudprovider.AWS.String())), &vars); err != nil { return clusterid.File{}, err } - defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}) - tfOutput, err := cl.CreateCluster(ctx) + defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}, opts.TFLogLevel) + tfOutput, err := cl.CreateCluster(ctx, opts.TFLogLevel) if err != nil { return clusterid.File{}, err } @@ -154,32 +167,30 @@ func (c *Creator) createAWS(ctx context.Context, cl terraformClient, config *con }, nil } -func (c *Creator) createGCP(ctx context.Context, cl terraformClient, config *config.Config, - insType string, controlPlaneCount, workerCount int, image string, -) (idFile clusterid.File, retErr error) { +func (c *Creator) createGCP(ctx context.Context, cl terraformClient, opts CreateOptions) (idFile clusterid.File, retErr error) { vars := terraform.GCPClusterVariables{ CommonVariables: terraform.CommonVariables{ - Name: config.Name, - CountControlPlanes: controlPlaneCount, - CountWorkers: workerCount, - StateDiskSizeGB: config.StateDiskSizeGB, + Name: opts.Config.Name, + CountControlPlanes: opts.ControlPlaneCount, + CountWorkers: opts.WorkerCount, + StateDiskSizeGB: opts.Config.StateDiskSizeGB, }, - Project: config.Provider.GCP.Project, - Region: config.Provider.GCP.Region, - Zone: config.Provider.GCP.Zone, - CredentialsFile: config.Provider.GCP.ServiceAccountKeyPath, - InstanceType: insType, - StateDiskType: config.Provider.GCP.StateDiskType, - ImageID: image, - Debug: config.IsDebugCluster(), + Project: opts.Config.Provider.GCP.Project, + Region: opts.Config.Provider.GCP.Region, + Zone: opts.Config.Provider.GCP.Zone, + CredentialsFile: opts.Config.Provider.GCP.ServiceAccountKeyPath, + InstanceType: opts.InsType, + StateDiskType: opts.Config.Provider.GCP.StateDiskType, + ImageID: opts.image, + Debug: opts.Config.IsDebugCluster(), } if err := cl.PrepareWorkspace(path.Join("terraform", strings.ToLower(cloudprovider.GCP.String())), &vars); err != nil { return clusterid.File{}, err } - defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}) - tfOutput, err := cl.CreateCluster(ctx) + defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}, opts.TFLogLevel) + tfOutput, err := cl.CreateCluster(ctx, opts.TFLogLevel) if err != nil { return clusterid.File{}, err } @@ -192,27 +203,26 @@ func (c *Creator) createGCP(ctx context.Context, cl terraformClient, config *con }, nil } -func (c *Creator) createAzure(ctx context.Context, cl terraformClient, config *config.Config, insType string, controlPlaneCount, workerCount int, image string, -) (idFile clusterid.File, retErr error) { +func (c *Creator) createAzure(ctx context.Context, cl terraformClient, opts CreateOptions) (idFile clusterid.File, retErr error) { vars := terraform.AzureClusterVariables{ CommonVariables: terraform.CommonVariables{ - Name: config.Name, - CountControlPlanes: controlPlaneCount, - CountWorkers: workerCount, - StateDiskSizeGB: config.StateDiskSizeGB, + Name: opts.Config.Name, + CountControlPlanes: opts.ControlPlaneCount, + CountWorkers: opts.WorkerCount, + StateDiskSizeGB: opts.Config.StateDiskSizeGB, }, - Location: config.Provider.Azure.Location, - ResourceGroup: config.Provider.Azure.ResourceGroup, - UserAssignedIdentity: config.Provider.Azure.UserAssignedIdentity, - InstanceType: insType, - StateDiskType: config.Provider.Azure.StateDiskType, - ImageID: image, - SecureBoot: *config.Provider.Azure.SecureBoot, - CreateMAA: config.Provider.Azure.EnforceIDKeyDigest == idkeydigest.MAAFallback, - Debug: config.IsDebugCluster(), + Location: opts.Config.Provider.Azure.Location, + ResourceGroup: opts.Config.Provider.Azure.ResourceGroup, + UserAssignedIdentity: opts.Config.Provider.Azure.UserAssignedIdentity, + InstanceType: opts.InsType, + StateDiskType: opts.Config.Provider.Azure.StateDiskType, + ImageID: opts.image, + SecureBoot: *opts.Config.Provider.Azure.SecureBoot, + CreateMAA: opts.Config.Provider.Azure.EnforceIDKeyDigest == idkeydigest.MAAFallback, + Debug: opts.Config.IsDebugCluster(), } - attestVariant, err := variant.FromString(config.AttestationVariant) + attestVariant, err := variant.FromString(opts.Config.AttestationVariant) if err != nil { return clusterid.File{}, fmt.Errorf("parsing attestation variant: %w", err) } @@ -224,8 +234,8 @@ func (c *Creator) createAzure(ctx context.Context, cl terraformClient, config *c return clusterid.File{}, err } - defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}) - tfOutput, err := cl.CreateCluster(ctx) + defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}, opts.TFLogLevel) + tfOutput, err := cl.CreateCluster(ctx, opts.TFLogLevel) if err != nil { return clusterid.File{}, err } @@ -348,14 +358,12 @@ func normalizeAzureURIs(vars terraform.AzureClusterVariables) terraform.AzureClu return vars } -func (c *Creator) createOpenStack(ctx context.Context, cl terraformClient, config *config.Config, - controlPlaneCount, workerCount int, image string, -) (idFile clusterid.File, retErr error) { +func (c *Creator) createOpenStack(ctx context.Context, cl terraformClient, opts CreateOptions) (idFile clusterid.File, retErr error) { // TODO: Remove this once OpenStack is supported. if os.Getenv("CONSTELLATION_OPENSTACK_DEV") != "1" { return clusterid.File{}, errors.New("OpenStack isn't supported yet") } - if _, hasOSAuthURL := os.LookupEnv("OS_AUTH_URL"); !hasOSAuthURL && config.Provider.OpenStack.Cloud == "" { + if _, hasOSAuthURL := os.LookupEnv("OS_AUTH_URL"); !hasOSAuthURL && opts.Config.Provider.OpenStack.Cloud == "" { return clusterid.File{}, errors.New( "neither environment variable OS_AUTH_URL nor cloud name for \"clouds.yaml\" is set. OpenStack authentication requires a set of " + "OS_* environment variables that are typically sourced into the current shell with an openrc file " + @@ -366,29 +374,29 @@ func (c *Creator) createOpenStack(ctx context.Context, cl terraformClient, confi vars := terraform.OpenStackClusterVariables{ CommonVariables: terraform.CommonVariables{ - Name: config.Name, - CountControlPlanes: controlPlaneCount, - CountWorkers: workerCount, - StateDiskSizeGB: config.StateDiskSizeGB, + Name: opts.Config.Name, + CountControlPlanes: opts.ControlPlaneCount, + CountWorkers: opts.WorkerCount, + StateDiskSizeGB: opts.Config.StateDiskSizeGB, }, - Cloud: config.Provider.OpenStack.Cloud, - AvailabilityZone: config.Provider.OpenStack.AvailabilityZone, - FloatingIPPoolID: config.Provider.OpenStack.FloatingIPPoolID, - FlavorID: config.Provider.OpenStack.FlavorID, - ImageURL: image, - DirectDownload: *config.Provider.OpenStack.DirectDownload, - OpenstackUserDomainName: config.Provider.OpenStack.UserDomainName, - OpenstackUsername: config.Provider.OpenStack.Username, - OpenstackPassword: config.Provider.OpenStack.Password, - Debug: config.IsDebugCluster(), + Cloud: opts.Config.Provider.OpenStack.Cloud, + AvailabilityZone: opts.Config.Provider.OpenStack.AvailabilityZone, + FloatingIPPoolID: opts.Config.Provider.OpenStack.FloatingIPPoolID, + FlavorID: opts.Config.Provider.OpenStack.FlavorID, + ImageURL: opts.image, + DirectDownload: *opts.Config.Provider.OpenStack.DirectDownload, + OpenstackUserDomainName: opts.Config.Provider.OpenStack.UserDomainName, + OpenstackUsername: opts.Config.Provider.OpenStack.Username, + OpenstackPassword: opts.Config.Provider.OpenStack.Password, + Debug: opts.Config.IsDebugCluster(), } if err := cl.PrepareWorkspace(path.Join("terraform", strings.ToLower(cloudprovider.OpenStack.String())), &vars); err != nil { return clusterid.File{}, err } - defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}) - tfOutput, err := cl.CreateCluster(ctx) + defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}, opts.TFLogLevel) + tfOutput, err := cl.CreateCluster(ctx, opts.TFLogLevel) if err != nil { return clusterid.File{}, err } @@ -401,26 +409,29 @@ func (c *Creator) createOpenStack(ctx context.Context, cl terraformClient, confi }, nil } -func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, lv libvirtRunner, config *config.Config, - controlPlaneCount, workerCount int, source string, -) (idFile clusterid.File, retErr error) { +type qemuCreateOptions struct { + source string + CreateOptions +} + +func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, lv libvirtRunner, opts qemuCreateOptions) (idFile clusterid.File, retErr error) { qemuRollbacker := &rollbackerQEMU{client: cl, libvirt: lv, createdWorkspace: false} - defer rollbackOnError(c.out, &retErr, qemuRollbacker) + defer rollbackOnError(c.out, &retErr, qemuRollbacker, opts.TFLogLevel) // TODO: render progress bar downloader := c.newRawDownloader() - imagePath, err := downloader.Download(ctx, c.out, false, source, config.Image) + imagePath, err := downloader.Download(ctx, c.out, false, opts.source, opts.Config.Image) if err != nil { return clusterid.File{}, fmt.Errorf("download raw image: %w", err) } - libvirtURI := config.Provider.QEMU.LibvirtURI + libvirtURI := opts.Config.Provider.QEMU.LibvirtURI libvirtSocketPath := "." switch { // if no libvirt URI is specified, start a libvirt container case libvirtURI == "": - if err := lv.Start(ctx, config.Name, config.Provider.QEMU.LibvirtContainerImage); err != nil { + if err := lv.Start(ctx, opts.Config.Name, opts.Config.Provider.QEMU.LibvirtContainerImage); err != nil { return clusterid.File{}, err } libvirtURI = libvirt.LibvirtTCPConnectURI @@ -452,21 +463,21 @@ func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, lv libvirt vars := terraform.QEMUVariables{ CommonVariables: terraform.CommonVariables{ - Name: config.Name, - CountControlPlanes: controlPlaneCount, - CountWorkers: workerCount, - StateDiskSizeGB: config.StateDiskSizeGB, + Name: opts.Config.Name, + CountControlPlanes: opts.ControlPlaneCount, + CountWorkers: opts.WorkerCount, + StateDiskSizeGB: opts.Config.StateDiskSizeGB, }, LibvirtURI: libvirtURI, LibvirtSocketPath: libvirtSocketPath, ImagePath: imagePath, - ImageFormat: config.Provider.QEMU.ImageFormat, - CPUCount: config.Provider.QEMU.VCPUs, - MemorySizeMiB: config.Provider.QEMU.Memory, - MetadataAPIImage: config.Provider.QEMU.MetadataAPIImage, + ImageFormat: opts.Config.Provider.QEMU.ImageFormat, + CPUCount: opts.Config.Provider.QEMU.VCPUs, + MemorySizeMiB: opts.Config.Provider.QEMU.Memory, + MetadataAPIImage: opts.Config.Provider.QEMU.MetadataAPIImage, MetadataLibvirtURI: metadataLibvirtURI, - NVRAM: config.Provider.QEMU.NVRAM, - Firmware: config.Provider.QEMU.Firmware, + NVRAM: opts.Config.Provider.QEMU.NVRAM, + Firmware: opts.Config.Provider.QEMU.Firmware, } if err := cl.PrepareWorkspace(path.Join("terraform", strings.ToLower(cloudprovider.QEMU.String())), &vars); err != nil { @@ -476,7 +487,7 @@ func (c *Creator) createQEMU(ctx context.Context, cl terraformClient, lv libvirt // Allow rollback of QEMU Terraform workspace from this point on qemuRollbacker.createdWorkspace = true - tfOutput, err := cl.CreateCluster(ctx) + tfOutput, err := cl.CreateCluster(ctx, opts.TFLogLevel) if err != nil { return clusterid.File{}, err } diff --git a/cli/internal/cloudcmd/create_test.go b/cli/internal/cloudcmd/create_test.go index f0e74c37c..765e70a64 100644 --- a/cli/internal/cloudcmd/create_test.go +++ b/cli/internal/cloudcmd/create_test.go @@ -215,7 +215,15 @@ func TestCreator(t *testing.T) { policyPatcher: tc.policyPatcher, } - idFile, err := creator.Create(context.Background(), tc.provider, tc.config, "type", 2, 3) + opts := CreateOptions{ + Provider: tc.provider, + Config: tc.config, + InsType: "type", + ControlPlaneCount: 2, + WorkerCount: 3, + TFLogLevel: terraform.LogLevelNone, + } + idFile, err := creator.Create(context.Background(), opts) if tc.wantErr { assert.Error(err) diff --git a/cli/internal/cloudcmd/iam.go b/cli/internal/cloudcmd/iam.go index f83a68eb5..cf3a9afde 100644 --- a/cli/internal/cloudcmd/iam.go +++ b/cli/internal/cloudcmd/iam.go @@ -70,8 +70,8 @@ func (d *IAMDestroyer) GetTfstateServiceAccountKey(ctx context.Context) (gcpshar } // 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 { +func (d *IAMDestroyer) DestroyIAMConfiguration(ctx context.Context, logLevel terraform.LogLevel) error { + if err := d.client.Destroy(ctx, logLevel); err != nil { return err } return d.client.CleanUpWorkspace() @@ -83,11 +83,12 @@ type IAMCreator struct { newTerraformClient func(ctx context.Context) (terraformClient, error) } -// IAMConfig holds the necessary values for IAM configuration. -type IAMConfig struct { - GCP GCPIAMConfig - Azure AzureIAMConfig - AWS AWSIAMConfig +// IAMConfigOptions holds the necessary values for IAM configuration. +type IAMConfigOptions struct { + GCP GCPIAMConfig + Azure AzureIAMConfig + AWS AWSIAMConfig + TFLogLevel terraform.LogLevel } // GCPIAMConfig holds the necessary values for GCP IAM configuration. @@ -122,7 +123,7 @@ func NewIAMCreator(out io.Writer) *IAMCreator { } // 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) { +func (c *IAMCreator) Create(ctx context.Context, provider cloudprovider.Provider, opts *IAMConfigOptions) (iamid.File, error) { switch provider { case cloudprovider.GCP: cl, err := c.newTerraformClient(ctx) @@ -130,42 +131,42 @@ func (c *IAMCreator) Create(ctx context.Context, provider cloudprovider.Provider return iamid.File{}, err } defer cl.RemoveInstaller() - return c.createGCP(ctx, cl, iamConfig) + return c.createGCP(ctx, cl, opts) case cloudprovider.Azure: cl, err := c.newTerraformClient(ctx) if err != nil { return iamid.File{}, err } defer cl.RemoveInstaller() - return c.createAzure(ctx, cl, iamConfig) + return c.createAzure(ctx, cl, opts) case cloudprovider.AWS: cl, err := c.newTerraformClient(ctx) if err != nil { return iamid.File{}, err } defer cl.RemoveInstaller() - return c.createAWS(ctx, cl, iamConfig) + return c.createAWS(ctx, cl, opts) 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(c.out, &retErr, &rollbackerTerraform{client: cl}) +func (c *IAMCreator) createGCP(ctx context.Context, cl terraformClient, opts *IAMConfigOptions) (retFile iamid.File, retErr error) { + defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}, opts.TFLogLevel) vars := terraform.GCPIAMVariables{ - ServiceAccountID: iamConfig.GCP.ServiceAccountID, - Project: iamConfig.GCP.ProjectID, - Region: iamConfig.GCP.Region, - Zone: iamConfig.GCP.Zone, + ServiceAccountID: opts.GCP.ServiceAccountID, + Project: opts.GCP.ProjectID, + Region: opts.GCP.Region, + Zone: opts.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) + iamOutput, err := cl.CreateIAMConfig(ctx, cloudprovider.GCP, opts.TFLogLevel) if err != nil { return iamid.File{}, err } @@ -179,20 +180,20 @@ func (c *IAMCreator) createGCP(ctx context.Context, cl terraformClient, iamConfi } // 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(c.out, &retErr, &rollbackerTerraform{client: cl}) +func (c *IAMCreator) createAzure(ctx context.Context, cl terraformClient, opts *IAMConfigOptions) (retFile iamid.File, retErr error) { + defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}, opts.TFLogLevel) vars := terraform.AzureIAMVariables{ - Region: iamConfig.Azure.Region, - ResourceGroup: iamConfig.Azure.ResourceGroup, - ServicePrincipal: iamConfig.Azure.ServicePrincipal, + Region: opts.Azure.Region, + ResourceGroup: opts.Azure.ResourceGroup, + ServicePrincipal: opts.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) + iamOutput, err := cl.CreateIAMConfig(ctx, cloudprovider.Azure, opts.TFLogLevel) if err != nil { return iamid.File{}, err } @@ -210,19 +211,19 @@ func (c *IAMCreator) createAzure(ctx context.Context, cl terraformClient, iamCon } // 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(c.out, &retErr, &rollbackerTerraform{client: cl}) +func (c *IAMCreator) createAWS(ctx context.Context, cl terraformClient, opts *IAMConfigOptions) (retFile iamid.File, retErr error) { + defer rollbackOnError(c.out, &retErr, &rollbackerTerraform{client: cl}, opts.TFLogLevel) vars := terraform.AWSIAMVariables{ - Region: iamConfig.AWS.Region, - Prefix: iamConfig.AWS.Prefix, + Region: opts.AWS.Region, + Prefix: opts.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) + iamOutput, err := cl.CreateIAMConfig(ctx, cloudprovider.AWS, opts.TFLogLevel) if err != nil { return iamid.File{}, err } diff --git a/cli/internal/cloudcmd/iam_test.go b/cli/internal/cloudcmd/iam_test.go index 923560e87..0a5e37113 100644 --- a/cli/internal/cloudcmd/iam_test.go +++ b/cli/internal/cloudcmd/iam_test.go @@ -89,7 +89,7 @@ func TestIAMCreator(t *testing.T) { testCases := map[string]struct { tfClient terraformClient newTfClientErr error - config *IAMConfig + config *IAMConfigOptions provider cloudprovider.Provider wantIAMIDFile iamid.File wantErr bool @@ -107,19 +107,19 @@ func TestIAMCreator(t *testing.T) { tfClient: &stubTerraformClient{iamOutput: validGCPIAMOutput}, wantIAMIDFile: validGCPIAMIDFile, provider: cloudprovider.GCP, - config: &IAMConfig{GCP: validGCPIAMConfig}, + config: &IAMConfigOptions{GCP: validGCPIAMConfig}, }, "azure": { tfClient: &stubTerraformClient{iamOutput: validAzureIAMOutput}, wantIAMIDFile: validAzureIAMIDFile, provider: cloudprovider.Azure, - config: &IAMConfig{Azure: validAzureIAMConfig}, + config: &IAMConfigOptions{Azure: validAzureIAMConfig}, }, "aws": { tfClient: &stubTerraformClient{iamOutput: validAWSIAMOutput}, wantIAMIDFile: validAWSIAMIDFile, provider: cloudprovider.AWS, - config: &IAMConfig{AWS: validAWSIAMConfig}, + config: &IAMConfigOptions{AWS: validAWSIAMConfig}, }, } @@ -188,7 +188,7 @@ func TestDestroyIAMConfiguration(t *testing.T) { assert := assert.New(t) destroyer := &IAMDestroyer{client: tc.tfClient} - err := destroyer.DestroyIAMConfiguration(context.Background()) + err := destroyer.DestroyIAMConfiguration(context.Background(), terraform.LogLevelNone) if tc.wantErr { assert.Error(err) diff --git a/cli/internal/cloudcmd/rollback.go b/cli/internal/cloudcmd/rollback.go index b98f65133..c2293cbe1 100644 --- a/cli/internal/cloudcmd/rollback.go +++ b/cli/internal/cloudcmd/rollback.go @@ -11,22 +11,24 @@ import ( "errors" "fmt" "io" + + "github.com/edgelesssys/constellation/v2/cli/internal/terraform" ) // rollbacker does a rollback. type rollbacker interface { - rollback(ctx context.Context) error + rollback(ctx context.Context, logLevel terraform.LogLevel) error } // rollbackOnError calls rollback on the rollbacker if the handed error is not nil, // and writes logs to the writer w. -func rollbackOnError(w io.Writer, onErr *error, roll rollbacker) { +func rollbackOnError(w io.Writer, onErr *error, roll rollbacker, logLevel terraform.LogLevel) { if *onErr == nil { return } fmt.Fprintf(w, "An error occurred: %s\n", *onErr) fmt.Fprintln(w, "Attempting to roll back.") - if err := roll.rollback(context.Background()); err != nil { + if err := roll.rollback(context.Background(), logLevel); err != nil { *onErr = errors.Join(*onErr, fmt.Errorf("on rollback: %w", err)) // TODO: print the error, or return it? return } @@ -37,8 +39,8 @@ type rollbackerTerraform struct { client terraformClient } -func (r *rollbackerTerraform) rollback(ctx context.Context) error { - if err := r.client.Destroy(ctx); err != nil { +func (r *rollbackerTerraform) rollback(ctx context.Context, logLevel terraform.LogLevel) error { + if err := r.client.Destroy(ctx, logLevel); err != nil { return err } return r.client.CleanUpWorkspace() @@ -50,9 +52,9 @@ type rollbackerQEMU struct { createdWorkspace bool } -func (r *rollbackerQEMU) rollback(ctx context.Context) (retErr error) { +func (r *rollbackerQEMU) rollback(ctx context.Context, logLevel terraform.LogLevel) (retErr error) { if r.createdWorkspace { - retErr = r.client.Destroy(ctx) + retErr = r.client.Destroy(ctx, logLevel) } if retErr := errors.Join(retErr, r.libvirt.Stop(ctx)); retErr != nil { return retErr diff --git a/cli/internal/cloudcmd/rollback_test.go b/cli/internal/cloudcmd/rollback_test.go index 42ce2c37e..07895a1fc 100644 --- a/cli/internal/cloudcmd/rollback_test.go +++ b/cli/internal/cloudcmd/rollback_test.go @@ -11,6 +11,7 @@ import ( "errors" "testing" + "github.com/edgelesssys/constellation/v2/cli/internal/terraform" "github.com/stretchr/testify/assert" ) @@ -42,7 +43,7 @@ func TestRollbackTerraform(t *testing.T) { client: tc.tfClient, } - err := rollbacker.rollback(context.Background()) + err := rollbacker.rollback(context.Background(), terraform.LogLevelNone) if tc.wantErr { assert.Error(err) if tc.tfClient.cleanUpWorkspaceErr == nil { @@ -98,7 +99,7 @@ func TestRollbackQEMU(t *testing.T) { createdWorkspace: tc.createdWorkspace, } - err := rollbacker.rollback(context.Background()) + err := rollbacker.rollback(context.Background(), terraform.LogLevelNone) if tc.wantErr { assert.Error(err) if tc.tfClient.cleanUpWorkspaceErr == nil { diff --git a/cli/internal/cloudcmd/terminate.go b/cli/internal/cloudcmd/terminate.go index 9918c6c99..f9fb3e3af 100644 --- a/cli/internal/cloudcmd/terminate.go +++ b/cli/internal/cloudcmd/terminate.go @@ -33,7 +33,7 @@ func NewTerminator() *Terminator { } // Terminate deletes the could provider resources. -func (t *Terminator) Terminate(ctx context.Context) (retErr error) { +func (t *Terminator) Terminate(ctx context.Context, logLevel terraform.LogLevel) (retErr error) { defer func() { if retErr == nil { retErr = t.newLibvirtRunner().Stop(ctx) @@ -46,11 +46,11 @@ func (t *Terminator) Terminate(ctx context.Context) (retErr error) { } defer cl.RemoveInstaller() - return t.terminateTerraform(ctx, cl) + return t.terminateTerraform(ctx, cl, logLevel) } -func (t *Terminator) terminateTerraform(ctx context.Context, cl terraformClient) error { - if err := cl.Destroy(ctx); err != nil { +func (t *Terminator) terminateTerraform(ctx context.Context, cl terraformClient, logLevel terraform.LogLevel) error { + if err := cl.Destroy(ctx, logLevel); err != nil { return err } return cl.CleanUpWorkspace() diff --git a/cli/internal/cloudcmd/terminate_test.go b/cli/internal/cloudcmd/terminate_test.go index f2ca53c53..4548d614d 100644 --- a/cli/internal/cloudcmd/terminate_test.go +++ b/cli/internal/cloudcmd/terminate_test.go @@ -11,6 +11,7 @@ import ( "errors" "testing" + "github.com/edgelesssys/constellation/v2/cli/internal/terraform" "github.com/stretchr/testify/assert" ) @@ -62,7 +63,7 @@ func TestTerminator(t *testing.T) { }, } - err := terminator.Terminate(context.Background()) + err := terminator.Terminate(context.Background(), terraform.LogLevelNone) if tc.wantErr { assert.Error(err) diff --git a/cli/internal/cmd/BUILD.bazel b/cli/internal/cmd/BUILD.bazel index 4a3f5a1f3..c37601bbd 100644 --- a/cli/internal/cmd/BUILD.bazel +++ b/cli/internal/cmd/BUILD.bazel @@ -120,6 +120,7 @@ go_test( "//cli/internal/helm", "//cli/internal/iamid", "//cli/internal/kubernetes", + "//cli/internal/terraform", "//disk-mapper/recoverproto", "//internal/atls", "//internal/attestation/measurements", diff --git a/cli/internal/cmd/cloud.go b/cli/internal/cmd/cloud.go index 3df89a7f3..6d8ed13f8 100644 --- a/cli/internal/cmd/cloud.go +++ b/cli/internal/cmd/cloud.go @@ -12,18 +12,15 @@ import ( "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" "github.com/edgelesssys/constellation/v2/cli/internal/clusterid" "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/cloud/gcpshared" - "github.com/edgelesssys/constellation/v2/internal/config" ) type cloudCreator interface { Create( ctx context.Context, - provider cloudprovider.Provider, - config *config.Config, - insType string, - coordCount, nodeCount int, + opts cloudcmd.CreateOptions, ) (clusterid.File, error) } @@ -31,15 +28,15 @@ type cloudIAMCreator interface { Create( ctx context.Context, provider cloudprovider.Provider, - iamConfig *cloudcmd.IAMConfig, + opts *cloudcmd.IAMConfigOptions, ) (iamid.File, error) } type iamDestroyer interface { - DestroyIAMConfiguration(ctx context.Context) error + DestroyIAMConfiguration(ctx context.Context, logLevel terraform.LogLevel) error GetTfstateServiceAccountKey(ctx context.Context) (gcpshared.ServiceAccountKey, error) } type cloudTerminator interface { - Terminate(context.Context) error + Terminate(ctx context.Context, logLevel terraform.LogLevel) error } diff --git a/cli/internal/cmd/cloud_test.go b/cli/internal/cmd/cloud_test.go index efef83aaf..38b127a88 100644 --- a/cli/internal/cmd/cloud_test.go +++ b/cli/internal/cmd/cloud_test.go @@ -13,9 +13,9 @@ import ( "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" "github.com/edgelesssys/constellation/v2/cli/internal/clusterid" "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/cloud/gcpshared" - "github.com/edgelesssys/constellation/v2/internal/config" "go.uber.org/goleak" ) @@ -34,13 +34,10 @@ type stubCloudCreator struct { func (c *stubCloudCreator) Create( _ context.Context, - provider cloudprovider.Provider, - _ *config.Config, - _ string, - _, _ int, + opts cloudcmd.CreateOptions, ) (clusterid.File, error) { c.createCalled = true - c.id.CloudProvider = provider + c.id.CloudProvider = opts.Provider return c.id, c.createErr } @@ -49,7 +46,7 @@ type stubCloudTerminator struct { terminateErr error } -func (c *stubCloudTerminator) Terminate(context.Context) error { +func (c *stubCloudTerminator) Terminate(_ context.Context, _ terraform.LogLevel) error { c.called = true return c.terminateErr } @@ -67,7 +64,7 @@ type stubIAMCreator struct { func (c *stubIAMCreator) Create( _ context.Context, provider cloudprovider.Provider, - _ *cloudcmd.IAMConfig, + _ *cloudcmd.IAMConfigOptions, ) (iamid.File, error) { c.createCalled = true c.id.CloudProvider = provider @@ -82,7 +79,7 @@ type stubIAMDestroyer struct { getTfstateKeyErr error } -func (d *stubIAMDestroyer) DestroyIAMConfiguration(_ context.Context) error { +func (d *stubIAMDestroyer) DestroyIAMConfiguration(_ context.Context, _ terraform.LogLevel) error { d.destroyCalled = true return d.destroyErr } diff --git a/cli/internal/cmd/create.go b/cli/internal/cmd/create.go index 8a85d98c2..a9bf863bd 100644 --- a/cli/internal/cmd/create.go +++ b/cli/internal/cmd/create.go @@ -153,7 +153,15 @@ func (c *createCmd) create(cmd *cobra.Command, creator cloudCreator, fileHandler } spinner.Start("Creating", false) - idFile, err := creator.Create(cmd.Context(), provider, conf, instanceType, flags.controllerCount, flags.workerCount) + opts := cloudcmd.CreateOptions{ + Provider: provider, + Config: conf, + InsType: instanceType, + ControlPlaneCount: flags.controllerCount, + WorkerCount: flags.workerCount, + TFLogLevel: flags.tfLogLevel, + } + idFile, err := creator.Create(cmd.Context(), opts) spinner.Stop() if err != nil { return translateCreateErrors(cmd, err) @@ -190,7 +198,7 @@ func (c *createCmd) parseCreateFlags(cmd *cobra.Command) (createFlags, error) { yes, err := cmd.Flags().GetBool("yes") if err != nil { - return createFlags{}, fmt.Errorf("%w; Set '-yes' without a value to automatically confirm", err) + return createFlags{}, fmt.Errorf("parsing yes bool: %w", err) } c.log.Debugf("Yes flag is %t", yes) @@ -206,10 +214,21 @@ func (c *createCmd) parseCreateFlags(cmd *cobra.Command) (createFlags, error) { } c.log.Debugf("force flag is %t", force) + logLevelString, err := cmd.Flags().GetString("tf-log") + if err != nil { + return createFlags{}, fmt.Errorf("parsing tf-log string: %w", err) + } + logLevel, err := terraform.ParseLogLevel(logLevelString) + if err != nil { + return createFlags{}, fmt.Errorf("parsing Terraform log level %s: %w", logLevelString, err) + } + c.log.Debugf("Terraform logs will be written into %s at level %s", constants.TerraformLogFile, logLevel.String()) + return createFlags{ controllerCount: controllerCount, workerCount: workerCount, configPath: configPath, + tfLogLevel: logLevel, force: force, yes: yes, }, nil @@ -220,6 +239,7 @@ type createFlags struct { controllerCount int workerCount int configPath string + tfLogLevel terraform.LogLevel force bool yes bool } diff --git a/cli/internal/cmd/create_test.go b/cli/internal/cmd/create_test.go index 9e1557c46..1ac36b39b 100644 --- a/cli/internal/cmd/create_test.go +++ b/cli/internal/cmd/create_test.go @@ -185,6 +185,7 @@ func TestCreate(t *testing.T) { cmd.SetIn(bytes.NewBufferString(tc.stdin)) cmd.Flags().String("config", constants.ConfigFilename, "") // register persistent flag manually cmd.Flags().Bool("force", true, "") // register persistent flag manually + cmd.Flags().String("tf-log", "NONE", "") // register persistent flag manually if tc.yesFlag { require.NoError(cmd.Flags().Set("yes", "true")) diff --git a/cli/internal/cmd/iamcreate.go b/cli/internal/cmd/iamcreate.go index 90c6533e0..1505c70f2 100644 --- a/cli/internal/cmd/iamcreate.go +++ b/cli/internal/cmd/iamcreate.go @@ -15,6 +15,7 @@ import ( "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" "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/config" "github.com/edgelesssys/constellation/v2/internal/constants" @@ -147,8 +148,18 @@ func createRunIAMFunc(provider cloudprovider.Provider) func(cmd *cobra.Command, return fmt.Errorf("unknown provider %s", provider) } } + return func(cmd *cobra.Command, args []string) error { - iamCreator, err := newIAMCreator(cmd) + logLevelString, err := cmd.Flags().GetString("tf-log") + if err != nil { + return fmt.Errorf("parsing tf-log string: %w", err) + } + logLevel, err := terraform.ParseLogLevel(logLevelString) + if err != nil { + return fmt.Errorf("parsing Terraform log level %s: %w", logLevelString, err) + } + + iamCreator, err := newIAMCreator(cmd, logLevel) if err != nil { return fmt.Errorf("creating iamCreator: %w", err) } @@ -161,7 +172,7 @@ func createRunIAMFunc(provider cloudprovider.Provider) func(cmd *cobra.Command, } // newIAMCreator creates a new iamiamCreator. -func newIAMCreator(cmd *cobra.Command) (*iamCreator, error) { +func newIAMCreator(cmd *cobra.Command, logLevel terraform.LogLevel) (*iamCreator, error) { spinner, err := newSpinnerOrStderr(cmd) if err != nil { return nil, fmt.Errorf("creating spinner: %w", err) @@ -170,13 +181,17 @@ func newIAMCreator(cmd *cobra.Command) (*iamCreator, error) { if err != nil { return nil, fmt.Errorf("creating logger: %w", err) } + log.Debugf("Terraform logs will be written into %s at level %s", constants.TerraformLogFile, logLevel.String()) + return &iamCreator{ cmd: cmd, spinner: spinner, log: log, creator: cloudcmd.NewIAMCreator(spinner), fileHandler: file.NewHandler(afero.NewOsFs()), - iamConfig: &cloudcmd.IAMConfig{}, + iamConfig: &cloudcmd.IAMConfigOptions{ + TFLogLevel: logLevel, + }, }, nil } @@ -188,7 +203,7 @@ type iamCreator struct { fileHandler file.Handler provider cloudprovider.Provider providerCreator providerIAMCreator - iamConfig *cloudcmd.IAMConfig + iamConfig *cloudcmd.IAMConfigOptions log debugLog } @@ -361,7 +376,7 @@ type providerIAMCreator interface { // writeOutputValuesToConfig writes the output values of the IAM creation to the constellation config file. writeOutputValuesToConfig(conf *config.Config, flags iamFlags, iamFile iamid.File) // parseFlagsAndSetupConfig parses the provider-specific flags and fills the values into the IAM config (output values of the command). - parseFlagsAndSetupConfig(cmd *cobra.Command, flags iamFlags, iamConfig *cloudcmd.IAMConfig) (iamFlags, error) + parseFlagsAndSetupConfig(cmd *cobra.Command, flags iamFlags, iamConfig *cloudcmd.IAMConfigOptions) (iamFlags, error) // parseAndWriteIDFile parses the GCP service account key and writes it to a keyfile. It is only implemented for GCP. parseAndWriteIDFile(iamFile iamid.File, fileHandler file.Handler) error } @@ -369,7 +384,7 @@ type providerIAMCreator interface { // awsIAMCreator implements the providerIAMCreator interface for AWS. type awsIAMCreator struct{} -func (c *awsIAMCreator) parseFlagsAndSetupConfig(cmd *cobra.Command, flags iamFlags, iamConfig *cloudcmd.IAMConfig) (iamFlags, error) { +func (c *awsIAMCreator) parseFlagsAndSetupConfig(cmd *cobra.Command, flags iamFlags, iamConfig *cloudcmd.IAMConfigOptions) (iamFlags, error) { prefix, err := cmd.Flags().GetString("prefix") if err != nil { return iamFlags{}, fmt.Errorf("parsing prefix string: %w", err) @@ -429,7 +444,7 @@ func (c *awsIAMCreator) parseAndWriteIDFile(_ iamid.File, _ file.Handler) error // azureIAMCreator implements the providerIAMCreator interface for Azure. type azureIAMCreator struct{} -func (c *azureIAMCreator) parseFlagsAndSetupConfig(cmd *cobra.Command, flags iamFlags, iamConfig *cloudcmd.IAMConfig) (iamFlags, error) { +func (c *azureIAMCreator) parseFlagsAndSetupConfig(cmd *cobra.Command, flags iamFlags, iamConfig *cloudcmd.IAMConfigOptions) (iamFlags, error) { region, err := cmd.Flags().GetString("region") if err != nil { return iamFlags{}, fmt.Errorf("parsing region string: %w", err) @@ -494,7 +509,7 @@ func (c *azureIAMCreator) parseAndWriteIDFile(_ iamid.File, _ file.Handler) erro // gcpIAMCreator implements the providerIAMCreator interface for GCP. type gcpIAMCreator struct{} -func (c *gcpIAMCreator) parseFlagsAndSetupConfig(cmd *cobra.Command, flags iamFlags, iamConfig *cloudcmd.IAMConfig) (iamFlags, error) { +func (c *gcpIAMCreator) parseFlagsAndSetupConfig(cmd *cobra.Command, flags iamFlags, iamConfig *cloudcmd.IAMConfigOptions) (iamFlags, error) { zone, err := cmd.Flags().GetString("zone") if err != nil { return iamFlags{}, fmt.Errorf("parsing zone string: %w", err) diff --git a/cli/internal/cmd/iamcreate_test.go b/cli/internal/cmd/iamcreate_test.go index 9aaf151d0..341f627b8 100644 --- a/cli/internal/cmd/iamcreate_test.go +++ b/cli/internal/cmd/iamcreate_test.go @@ -278,6 +278,7 @@ func TestIAMCreateAWS(t *testing.T) { cmd.Flags().String("kubernetes", semver.MajorMinor(config.Default().KubernetesVersion), "") cmd.Flags().Bool("yes", false, "") cmd.Flags().String("name", "constell", "") + cmd.Flags().String("tf-log", "NONE", "") if tc.zoneFlag != "" { require.NoError(cmd.Flags().Set("zone", tc.zoneFlag)) @@ -306,7 +307,7 @@ func TestIAMCreateAWS(t *testing.T) { spinner: &nopSpinner{}, creator: tc.creator, fileHandler: fileHandler, - iamConfig: &cloudcmd.IAMConfig{}, + iamConfig: &cloudcmd.IAMConfigOptions{}, provider: tc.provider, providerCreator: &awsIAMCreator{}, } @@ -550,12 +551,13 @@ func TestIAMCreateAzure(t *testing.T) { cmd.SetErr(&bytes.Buffer{}) cmd.SetIn(bytes.NewBufferString(tc.stdin)) - // register persistent flag manually + // register persistent flags manually cmd.Flags().String("config", constants.ConfigFilename, "") cmd.Flags().Bool("generate-config", false, "") cmd.Flags().String("kubernetes", semver.MajorMinor(config.Default().KubernetesVersion), "") cmd.Flags().Bool("yes", false, "") cmd.Flags().String("name", "constell", "") + cmd.Flags().String("tf-log", "NONE", "") if tc.regionFlag != "" { require.NoError(cmd.Flags().Set("region", tc.regionFlag)) @@ -587,7 +589,7 @@ func TestIAMCreateAzure(t *testing.T) { spinner: &nopSpinner{}, creator: tc.creator, fileHandler: fileHandler, - iamConfig: &cloudcmd.IAMConfig{}, + iamConfig: &cloudcmd.IAMConfigOptions{}, provider: tc.provider, providerCreator: &azureIAMCreator{}, } @@ -862,6 +864,7 @@ func TestIAMCreateGCP(t *testing.T) { cmd.Flags().String("kubernetes", semver.MajorMinor(config.Default().KubernetesVersion), "") cmd.Flags().Bool("yes", false, "") cmd.Flags().String("name", "constell", "") + cmd.Flags().String("tf-log", "NONE", "") if tc.zoneFlag != "" { require.NoError(cmd.Flags().Set("zone", tc.zoneFlag)) @@ -893,7 +896,7 @@ func TestIAMCreateGCP(t *testing.T) { spinner: &nopSpinner{}, creator: tc.creator, fileHandler: fileHandler, - iamConfig: &cloudcmd.IAMConfig{}, + iamConfig: &cloudcmd.IAMConfigOptions{}, provider: tc.provider, providerCreator: &gcpIAMCreator{}, } diff --git a/cli/internal/cmd/iamdestroy.go b/cli/internal/cmd/iamdestroy.go index 9b40feb40..fc1a1d39a 100644 --- a/cli/internal/cmd/iamdestroy.go +++ b/cli/internal/cmd/iamdestroy.go @@ -11,6 +11,7 @@ import ( "os" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" + "github.com/edgelesssys/constellation/v2/cli/internal/terraform" "github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" @@ -56,9 +57,14 @@ type destroyCmd struct { } func (c *destroyCmd) iamDestroy(cmd *cobra.Command, spinner spinnerInterf, destroyer iamDestroyer, fsHandler file.Handler) error { + flags, err := c.parseDestroyFlags(cmd) + if err != nil { + return fmt.Errorf("parsing flags: %w", err) + } + // 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) + _, 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) } @@ -68,12 +74,6 @@ func (c *destroyCmd) iamDestroy(cmd *cobra.Command, spinner spinnerInterf, destr 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) @@ -87,7 +87,7 @@ func (c *destroyCmd) iamDestroy(cmd *cobra.Command, spinner spinnerInterf, destr gcpFileExists = true } - if !yes { + if !flags.yes { // Confirmation confirmString := "Do you really want to destroy your IAM configuration?" if gcpFileExists { @@ -119,7 +119,7 @@ func (c *destroyCmd) iamDestroy(cmd *cobra.Command, spinner spinnerInterf, destr spinner.Start("Destroying IAM configuration", false) defer spinner.Stop() - if err := destroyer.DestroyIAMConfiguration(cmd.Context()); err != nil { + if err := destroyer.DestroyIAMConfiguration(cmd.Context(), flags.tfLogLevel); err != nil { return fmt.Errorf("destroying IAM configuration: %w", err) } @@ -155,3 +155,32 @@ func (c *destroyCmd) deleteGCPServiceAccountKeyFile(cmd *cobra.Command, destroye c.log.Debugf("Successfully deleted %q", constants.GCPServiceAccountKeyFile) return true, nil } + +type destroyFlags struct { + yes bool + tfLogLevel terraform.LogLevel +} + +// parseDestroyFlags parses the flags of the create command. +func (c *destroyCmd) parseDestroyFlags(cmd *cobra.Command) (destroyFlags, error) { + yes, err := cmd.Flags().GetBool("yes") + if err != nil { + return destroyFlags{}, fmt.Errorf("parsing yes bool: %w", err) + } + c.log.Debugf("Yes flag is %t", yes) + + logLevelString, err := cmd.Flags().GetString("tf-log") + if err != nil { + return destroyFlags{}, fmt.Errorf("parsing tf-log string: %w", err) + } + logLevel, err := terraform.ParseLogLevel(logLevelString) + if err != nil { + return destroyFlags{}, fmt.Errorf("parsing Terraform log level %s: %w", logLevelString, err) + } + c.log.Debugf("Terraform logs will be written into %s at level %s", constants.TerraformLogFile, logLevel.String()) + + return destroyFlags{ + tfLogLevel: logLevel, + yes: yes, + }, nil +} diff --git a/cli/internal/cmd/iamdestroy_test.go b/cli/internal/cmd/iamdestroy_test.go index 53e39a6a7..9ea5a2059 100644 --- a/cli/internal/cmd/iamdestroy_test.go +++ b/cli/internal/cmd/iamdestroy_test.go @@ -105,6 +105,10 @@ func TestIAMDestroy(t *testing.T) { cmd.SetOut(&bytes.Buffer{}) cmd.SetErr(&bytes.Buffer{}) cmd.SetIn(bytes.NewBufferString(tc.stdin)) + + // register persistent flags manually + cmd.Flags().String("tf-log", "NONE", "") + assert.NoError(cmd.Flags().Set("yes", tc.yesFlag)) c := &destroyCmd{log: logger.NewTest(t)} diff --git a/cli/internal/cmd/miniup.go b/cli/internal/cmd/miniup.go index c0cf741ae..ccb15b14c 100644 --- a/cli/internal/cmd/miniup.go +++ b/cli/internal/cmd/miniup.go @@ -19,6 +19,7 @@ import ( "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" "github.com/edgelesssys/constellation/v2/cli/internal/libvirt" + "github.com/edgelesssys/constellation/v2/cli/internal/terraform" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" "github.com/edgelesssys/constellation/v2/internal/config" "github.com/edgelesssys/constellation/v2/internal/constants" @@ -73,17 +74,22 @@ func (m *miniUpCmd) up(cmd *cobra.Command, creator cloudCreator, spinner spinner return fmt.Errorf("system requirements not met: %w", err) } + flags, err := m.parseUpFlags(cmd) + if err != nil { + return fmt.Errorf("parsing flags: %w", err) + } + fileHandler := file.NewHandler(afero.NewOsFs()) // create config if not passed as flag and set default values - config, err := m.prepareConfig(cmd, fileHandler) + config, err := m.prepareConfig(cmd, fileHandler, flags) if err != nil { return fmt.Errorf("preparing config: %w", err) } // create cluster spinner.Start("Creating cluster in QEMU ", false) - err = m.createMiniCluster(cmd.Context(), fileHandler, creator, config) + err = m.createMiniCluster(cmd.Context(), fileHandler, creator, config, flags.tfLogLevel) spinner.Stop() if err != nil { return fmt.Errorf("creating cluster: %w", err) @@ -173,20 +179,10 @@ func (m *miniUpCmd) checkSystemRequirements(out io.Writer) error { } // prepareConfig reads a given config, or creates a new minimal QEMU config. -func (m *miniUpCmd) prepareConfig(cmd *cobra.Command, fileHandler file.Handler) (*config.Config, error) { - m.log.Debugf("Preparing configuration") - configPath, err := cmd.Flags().GetString("config") - if err != nil { - return nil, err - } - force, err := cmd.Flags().GetBool("force") - if err != nil { - return nil, fmt.Errorf("parsing force argument: %w", err) - } - +func (m *miniUpCmd) prepareConfig(cmd *cobra.Command, fileHandler file.Handler, flags upFlags) (*config.Config, error) { // check for existing config - if configPath != "" { - conf, err := config.New(fileHandler, configPath, force) + if flags.configPath != "" { + conf, err := config.New(fileHandler, flags.configPath, flags.force) var configValidationErr *config.ValidationError if errors.As(err, &configValidationErr) { cmd.PrintErrln(configValidationErr.LongMessage()) @@ -199,11 +195,11 @@ func (m *miniUpCmd) prepareConfig(cmd *cobra.Command, fileHandler file.Handler) } return conf, nil } - m.log.Debugf("Configuration path is %q", configPath) + m.log.Debugf("Configuration path is %q", flags.configPath) if err := cmd.Flags().Set("config", constants.ConfigFilename); err != nil { return nil, err } - _, err = fileHandler.Stat(constants.ConfigFilename) + _, err := fileHandler.Stat(constants.ConfigFilename) if err == nil { // config already exists, prompt user to overwrite cmd.PrintErrln("A config file already exists in the current workspace. Use --config to use an existing config file.") @@ -227,9 +223,17 @@ func (m *miniUpCmd) prepareConfig(cmd *cobra.Command, fileHandler file.Handler) } // createMiniCluster creates a new cluster using the given config. -func (m *miniUpCmd) createMiniCluster(ctx context.Context, fileHandler file.Handler, creator cloudCreator, config *config.Config) error { +func (m *miniUpCmd) createMiniCluster(ctx context.Context, fileHandler file.Handler, creator cloudCreator, config *config.Config, tfLogLevel terraform.LogLevel) error { m.log.Debugf("Creating mini cluster") - idFile, err := creator.Create(ctx, cloudprovider.QEMU, config, "", 1, 1) + opts := cloudcmd.CreateOptions{ + Provider: cloudprovider.QEMU, + Config: config, + InsType: "", + ControlPlaneCount: 1, + WorkerCount: 1, + TFLogLevel: tfLogLevel, + } + idFile, err := creator.Create(ctx, opts) if err != nil { return err } @@ -271,3 +275,39 @@ func (m *miniUpCmd) initializeMiniCluster(cmd *cobra.Command, fileHandler file.H m.log.Debugf("Initialized mini cluster") return nil } + +type upFlags struct { + configPath string + force bool + tfLogLevel terraform.LogLevel +} + +func (m *miniUpCmd) parseUpFlags(cmd *cobra.Command) (upFlags, error) { + m.log.Debugf("Preparing configuration") + configPath, err := cmd.Flags().GetString("config") + if err != nil { + return upFlags{}, fmt.Errorf("parsing config string: %w", err) + } + m.log.Debugf("Configuration path is %q", configPath) + force, err := cmd.Flags().GetBool("force") + if err != nil { + return upFlags{}, fmt.Errorf("parsing force bool: %w", err) + } + m.log.Debugf("force flag is %q", configPath) + + logLevelString, err := cmd.Flags().GetString("tf-log") + if err != nil { + return upFlags{}, fmt.Errorf("parsing tf-log string: %w", err) + } + logLevel, err := terraform.ParseLogLevel(logLevelString) + if err != nil { + return upFlags{}, fmt.Errorf("parsing Terraform log level %s: %w", logLevelString, err) + } + m.log.Debugf("Terraform logs will be written into %s at level %s", constants.TerraformLogFile, logLevel.String()) + + return upFlags{ + configPath: configPath, + force: force, + tfLogLevel: logLevel, + }, nil +} diff --git a/cli/internal/cmd/terminate.go b/cli/internal/cmd/terminate.go index cacbd5d79..ff50c924f 100644 --- a/cli/internal/cmd/terminate.go +++ b/cli/internal/cmd/terminate.go @@ -15,6 +15,7 @@ import ( "github.com/spf13/cobra" "github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd" + "github.com/edgelesssys/constellation/v2/cli/internal/terraform" "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" ) @@ -48,12 +49,12 @@ func runTerminate(cmd *cobra.Command, _ []string) error { func terminate(cmd *cobra.Command, terminator cloudTerminator, fileHandler file.Handler, spinner spinnerInterf, ) error { - yesFlag, err := cmd.Flags().GetBool("yes") + flags, err := parseTerminateFlags(cmd) if err != nil { - return err + return fmt.Errorf("parsing flags: %w", err) } - if !yesFlag { + if !flags.yes { cmd.Println("You are about to terminate a Constellation cluster.") cmd.Println("All of its associated resources will be DESTROYED.") cmd.Println("This action is irreversible and ALL DATA WILL BE LOST.") @@ -68,7 +69,7 @@ func terminate(cmd *cobra.Command, terminator cloudTerminator, fileHandler file. } spinner.Start("Terminating", false) - err = terminator.Terminate(cmd.Context()) + err = terminator.Terminate(cmd.Context(), flags.logLevel) spinner.Stop() if err != nil { return fmt.Errorf("terminating Constellation cluster: %w", err) @@ -87,3 +88,28 @@ func terminate(cmd *cobra.Command, terminator cloudTerminator, fileHandler file. return removeErr } + +type terminateFlags struct { + yes bool + logLevel terraform.LogLevel +} + +func parseTerminateFlags(cmd *cobra.Command) (terminateFlags, error) { + yes, err := cmd.Flags().GetBool("yes") + if err != nil { + return terminateFlags{}, fmt.Errorf("parsing yes bool: %w", err) + } + logLevelString, err := cmd.Flags().GetString("tf-log") + if err != nil { + return terminateFlags{}, fmt.Errorf("parsing tf-log string: %w", err) + } + logLevel, err := terraform.ParseLogLevel(logLevelString) + if err != nil { + return terminateFlags{}, fmt.Errorf("parsing Terraform log level %s: %w", logLevelString, err) + } + + return terminateFlags{ + yes: yes, + logLevel: logLevel, + }, nil +} diff --git a/cli/internal/cmd/terminate_test.go b/cli/internal/cmd/terminate_test.go index 45334c481..504b98ef8 100644 --- a/cli/internal/cmd/terminate_test.go +++ b/cli/internal/cmd/terminate_test.go @@ -135,6 +135,9 @@ func TestTerminate(t *testing.T) { cmd.SetErr(&bytes.Buffer{}) cmd.SetIn(bytes.NewBufferString(tc.stdin)) + // register persistent flags manually + cmd.Flags().String("tf-log", "NONE", "") + require.NotNil(tc.setupFs) fileHandler := file.NewHandler(tc.setupFs(require, tc.idFile)) diff --git a/cli/internal/terraform/BUILD.bazel b/cli/internal/terraform/BUILD.bazel index 998c3b468..9d1a9ad59 100644 --- a/cli/internal/terraform/BUILD.bazel +++ b/cli/internal/terraform/BUILD.bazel @@ -5,6 +5,7 @@ go_library( name = "terraform", srcs = [ "loader.go", + "logging.go", "terraform.go", "variables.go", ], @@ -73,6 +74,7 @@ go_library( visibility = ["//cli:__subpackages__"], deps = [ "//internal/cloud/cloudprovider", + "//internal/constants", "//internal/file", "@com_github_hashicorp_go_version//:go-version", "@com_github_hashicorp_hc_install//:hc-install", diff --git a/cli/internal/terraform/logging.go b/cli/internal/terraform/logging.go new file mode 100644 index 000000000..6a400fb03 --- /dev/null +++ b/cli/internal/terraform/logging.go @@ -0,0 +1,75 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package terraform + +import ( + "fmt" + "strings" +) + +const ( + // LogLevelNone represents a log level that does not produce any output. + LogLevelNone LogLevel = iota + // LogLevelError enables log output at ERROR level. + LogLevelError + // LogLevelWarn enables log output at WARN level. + LogLevelWarn + // LogLevelInfo enables log output at INFO level. + LogLevelInfo + // LogLevelDebug enables log output at DEBUG level. + LogLevelDebug + // LogLevelTrace enables log output at TRACE level. + LogLevelTrace + // LogLevelJSON enables log output at TRACE level in JSON format. + LogLevelJSON +) + +// LogLevel is a Terraform log level. +// As per https://developer.hashicorp.com/terraform/internals/debugging +type LogLevel int + +// ParseLogLevel parses a log level string into a Terraform log level. +func ParseLogLevel(level string) (LogLevel, error) { + switch strings.ToUpper(level) { + case "NONE": + return LogLevelNone, nil + case "ERROR": + return LogLevelError, nil + case "WARN": + return LogLevelWarn, nil + case "INFO": + return LogLevelInfo, nil + case "DEBUG": + return LogLevelDebug, nil + case "TRACE": + return LogLevelTrace, nil + case "JSON": + return LogLevelJSON, nil + default: + return LogLevelNone, fmt.Errorf("invalid log level %s", level) + } +} + +// String returns the string representation of a Terraform log level. +func (l LogLevel) String() string { + switch l { + case LogLevelError: + return "ERROR" + case LogLevelWarn: + return "WARN" + case LogLevelInfo: + return "INFO" + case LogLevelDebug: + return "DEBUG" + case LogLevelTrace: + return "TRACE" + case LogLevelJSON: + return "JSON" + default: + return "" + } +} diff --git a/cli/internal/terraform/terraform.go b/cli/internal/terraform/terraform.go index 0defba4ad..0691bac99 100644 --- a/cli/internal/terraform/terraform.go +++ b/cli/internal/terraform/terraform.go @@ -17,9 +17,11 @@ package terraform import ( "context" "errors" + "fmt" "path/filepath" "github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider" + "github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/file" "github.com/hashicorp/go-version" install "github.com/hashicorp/hc-install" @@ -83,18 +85,22 @@ func (c *Client) PrepareWorkspace(path string, vars Variables) error { } // CreateCluster creates a Constellation cluster using Terraform. -func (c *Client) CreateCluster(ctx context.Context) (CreateOutput, error) { +func (c *Client) CreateCluster(ctx context.Context, logLevel LogLevel) (CreateOutput, error) { + if err := c.setLogLevel(logLevel); err != nil { + return CreateOutput{}, fmt.Errorf("set terraform log level %s: %w", logLevel.String(), err) + } + if err := c.tf.Init(ctx); err != nil { - return CreateOutput{}, err + return CreateOutput{}, fmt.Errorf("terraform init: %w", err) } if err := c.tf.Apply(ctx); err != nil { - return CreateOutput{}, err + return CreateOutput{}, fmt.Errorf("terraform apply: %w", err) } tfState, err := c.tf.Show(ctx) if err != nil { - return CreateOutput{}, err + return CreateOutput{}, fmt.Errorf("terraform show: %w", err) } ipOutput, ok := tfState.Values.Outputs["ip"] @@ -177,7 +183,11 @@ type AWSIAMOutput struct { } // CreateIAMConfig creates an IAM configuration using Terraform. -func (c *Client) CreateIAMConfig(ctx context.Context, provider cloudprovider.Provider) (IAMOutput, error) { +func (c *Client) CreateIAMConfig(ctx context.Context, provider cloudprovider.Provider, logLevel LogLevel) (IAMOutput, error) { + if err := c.setLogLevel(logLevel); err != nil { + return IAMOutput{}, fmt.Errorf("set terraform log level %s: %w", logLevel.String(), err) + } + if err := c.tf.Init(ctx); err != nil { return IAMOutput{}, err } @@ -285,9 +295,13 @@ func (c *Client) CreateIAMConfig(ctx context.Context, provider cloudprovider.Pro } // Destroy destroys Terraform-created cloud resources. -func (c *Client) Destroy(ctx context.Context) error { +func (c *Client) Destroy(ctx context.Context, logLevel LogLevel) error { + if err := c.setLogLevel(logLevel); err != nil { + return fmt.Errorf("set terraform log level %s: %w", logLevel.String(), err) + } + if err := c.tf.Init(ctx); err != nil { - return err + return fmt.Errorf("terraform init: %w", err) } return c.tf.Destroy(ctx) } @@ -354,9 +368,24 @@ func (c *Client) writeVars(vars Variables) error { return nil } +// setLogLevel sets the log level for Terraform. +func (c *Client) setLogLevel(logLevel LogLevel) error { + if logLevel.String() != "" { + if err := c.tf.SetLog(logLevel.String()); err != nil { + return fmt.Errorf("set log level %s: %w", logLevel.String(), err) + } + if err := c.tf.SetLogPath(filepath.Join("..", constants.TerraformLogFile)); err != nil { + return fmt.Errorf("set log path: %w", err) + } + } + return nil +} + type tfInterface interface { Apply(context.Context, ...tfexec.ApplyOption) error Destroy(context.Context, ...tfexec.DestroyOption) error Init(context.Context, ...tfexec.InitOption) error Show(context.Context, ...tfexec.ShowOption) (*tfjson.State, error) + SetLog(level string) error + SetLogPath(path string) error } diff --git a/cli/internal/terraform/terraform_test.go b/cli/internal/terraform/terraform_test.go index 43f443359..66094179f 100644 --- a/cli/internal/terraform/terraform_test.go +++ b/cli/internal/terraform/terraform_test.go @@ -299,6 +299,22 @@ func TestCreateCluster(t *testing.T) { fs: afero.NewMemMapFs(), wantErr: true, }, + "set log fails": { + pathBase: "terraform", + provider: cloudprovider.QEMU, + vars: qemuVars, + tf: &stubTerraform{setLogErr: someErr}, + fs: afero.NewMemMapFs(), + wantErr: true, + }, + "set log path fails": { + pathBase: "terraform", + provider: cloudprovider.QEMU, + vars: qemuVars, + tf: &stubTerraform{setLogPathErr: someErr}, + fs: afero.NewMemMapFs(), + wantErr: true, + }, "no ip": { pathBase: "terraform", provider: cloudprovider.QEMU, @@ -406,7 +422,7 @@ func TestCreateCluster(t *testing.T) { path := path.Join(tc.pathBase, strings.ToLower(tc.provider.String())) require.NoError(c.PrepareWorkspace(path, tc.vars)) - tfOutput, err := c.CreateCluster(context.Background()) + tfOutput, err := c.CreateCluster(context.Background(), LogLevelDebug) if tc.wantErr { assert.Error(err) @@ -481,6 +497,22 @@ func TestCreateIAM(t *testing.T) { wantErr bool want IAMOutput }{ + "set log fails": { + pathBase: path.Join("terraform", "iam"), + provider: cloudprovider.GCP, + vars: gcpVars, + tf: &stubTerraform{setLogErr: someErr}, + fs: afero.NewMemMapFs(), + wantErr: true, + }, + "set log path fails": { + pathBase: path.Join("terraform", "iam"), + provider: cloudprovider.GCP, + vars: gcpVars, + tf: &stubTerraform{setLogPathErr: someErr}, + fs: afero.NewMemMapFs(), + wantErr: true, + }, "gcp works": { pathBase: path.Join("terraform", "iam"), provider: cloudprovider.GCP, @@ -685,7 +717,7 @@ func TestCreateIAM(t *testing.T) { 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) + IAMoutput, err := c.CreateIAMConfig(context.Background(), tc.provider, LogLevelDebug) if tc.wantErr { assert.Error(err) @@ -698,6 +730,7 @@ func TestCreateIAM(t *testing.T) { } func TestDestroyInstances(t *testing.T) { + someErr := errors.New("some error") testCases := map[string]struct { tf *stubTerraform wantErr bool @@ -706,9 +739,15 @@ func TestDestroyInstances(t *testing.T) { tf: &stubTerraform{}, }, "destroy fails": { - tf: &stubTerraform{ - destroyErr: errors.New("error"), - }, + tf: &stubTerraform{destroyErr: someErr}, + wantErr: true, + }, + "setLog fails": { + tf: &stubTerraform{setLogErr: someErr}, + wantErr: true, + }, + "setLogPath fails": { + tf: &stubTerraform{setLogPathErr: someErr}, wantErr: true, }, } @@ -721,7 +760,7 @@ func TestDestroyInstances(t *testing.T) { tf: tc.tf, } - err := c.Destroy(context.Background()) + err := c.Destroy(context.Background(), LogLevelDebug) if tc.wantErr { assert.Error(err) return @@ -788,12 +827,121 @@ func TestCleanupWorkspace(t *testing.T) { } } +func TestParseLogLevel(t *testing.T) { + testCases := map[string]struct { + level string + want LogLevel + wantErr bool + }{ + "json": { + level: "json", + want: LogLevelJSON, + }, + "trace": { + level: "trace", + want: LogLevelTrace, + }, + "debug": { + level: "debug", + want: LogLevelDebug, + }, + "info": { + level: "info", + want: LogLevelInfo, + }, + "warn": { + level: "warn", + want: LogLevelWarn, + }, + "error": { + level: "error", + want: LogLevelError, + }, + "none": { + level: "none", + want: LogLevelNone, + }, + "unknown": { + level: "unknown", + wantErr: true, + }, + "empty": { + level: "", + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + level, err := ParseLogLevel(tc.level) + if tc.wantErr { + assert.Error(err) + return + } + assert.NoError(err) + assert.Equal(tc.want, level) + }) + } +} + +func TestLogLevelString(t *testing.T) { + testCases := map[string]struct { + level LogLevel + want string + }{ + "json": { + level: LogLevelJSON, + want: "JSON", + }, + "trace": { + level: LogLevelTrace, + want: "TRACE", + }, + "debug": { + level: LogLevelDebug, + want: "DEBUG", + }, + "info": { + level: LogLevelInfo, + want: "INFO", + }, + "warn": { + level: LogLevelWarn, + want: "WARN", + }, + "error": { + level: LogLevelError, + want: "ERROR", + }, + "none": { + level: LogLevelNone, + want: "", + }, + "invalid int": { + level: LogLevel(-1), + want: "", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + assert.Equal(tc.want, tc.level.String()) + }) + } +} + type stubTerraform struct { - applyErr error - destroyErr error - initErr error - showErr error - showState *tfjson.State + applyErr error + destroyErr error + initErr error + showErr error + setLogErr error + setLogPathErr error + showState *tfjson.State } func (s *stubTerraform) Apply(context.Context, ...tfexec.ApplyOption) error { @@ -811,3 +959,11 @@ func (s *stubTerraform) Init(context.Context, ...tfexec.InitOption) error { func (s *stubTerraform) Show(context.Context, ...tfexec.ShowOption) (*tfjson.State, error) { return s.showState, s.showErr } + +func (s *stubTerraform) SetLog(_ string) error { + return s.setLogErr +} + +func (s *stubTerraform) SetLogPath(_ string) error { + return s.setLogPathErr +} diff --git a/docs/docs/reference/cli.md b/docs/docs/reference/cli.md index d39f9191f..5560cc4e7 100644 --- a/docs/docs/reference/cli.md +++ b/docs/docs/reference/cli.md @@ -56,6 +56,7 @@ Work with the Constellation configuration file. --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation config generate @@ -84,6 +85,7 @@ constellation config generate {aws|azure|gcp|openstack|qemu} [flags] --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation config fetch-measurements @@ -114,6 +116,7 @@ constellation config fetch-measurements [flags] --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation config instance-types @@ -140,6 +143,7 @@ constellation config instance-types [flags] --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation config kubernetes-versions @@ -166,6 +170,7 @@ constellation config kubernetes-versions [flags] --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation create @@ -195,6 +200,7 @@ constellation create [flags] --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation init @@ -226,6 +232,7 @@ constellation init [flags] --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation mini @@ -248,6 +255,7 @@ Manage MiniConstellation clusters. --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation mini up @@ -275,8 +283,9 @@ constellation mini up [flags] ### Options inherited from parent commands ``` - --debug enable debug logging - --force disable version compatibility checks - might result in corrupted clusters + --debug enable debug logging + --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation mini down @@ -304,6 +313,7 @@ constellation mini down [flags] --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation verify @@ -333,6 +343,7 @@ constellation verify [flags] --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation upgrade @@ -355,6 +366,7 @@ Find and apply upgrades to your Constellation cluster. --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation upgrade check @@ -384,6 +396,7 @@ constellation upgrade check [flags] --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation upgrade apply @@ -413,6 +426,7 @@ constellation upgrade apply [flags] --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation recover @@ -443,6 +457,7 @@ constellation recover [flags] --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation terminate @@ -472,6 +487,7 @@ constellation terminate [flags] --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation version @@ -498,6 +514,7 @@ constellation version [flags] --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation iam @@ -520,6 +537,7 @@ Work with the IAM configuration on your cloud provider. --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation iam create @@ -545,6 +563,7 @@ Create IAM configuration on a cloud platform for your Constellation cluster. --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation iam create aws @@ -576,6 +595,7 @@ constellation iam create aws [flags] --force disable version compatibility checks - might result in corrupted clusters --generate-config automatically generate a configuration file and fill in the required fields -k, --kubernetes string Kubernetes version to use in format MAJOR.MINOR - only usable in combination with --generate-config (default "v1.25") + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") -y, --yes create the IAM configuration without further confirmation ``` @@ -608,6 +628,7 @@ constellation iam create azure [flags] --force disable version compatibility checks - might result in corrupted clusters --generate-config automatically generate a configuration file and fill in the required fields -k, --kubernetes string Kubernetes version to use in format MAJOR.MINOR - only usable in combination with --generate-config (default "v1.25") + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") -y, --yes create the IAM configuration without further confirmation ``` @@ -643,6 +664,7 @@ constellation iam create gcp [flags] --force disable version compatibility checks - might result in corrupted clusters --generate-config automatically generate a configuration file and fill in the required fields -k, --kubernetes string Kubernetes version to use in format MAJOR.MINOR - only usable in combination with --generate-config (default "v1.25") + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") -y, --yes create the IAM configuration without further confirmation ``` @@ -671,6 +693,7 @@ constellation iam destroy [flags] --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` ## constellation status @@ -699,5 +722,6 @@ constellation status [flags] --config string path to the configuration file (default "constellation-conf.yaml") --debug enable debug logging --force disable version compatibility checks - might result in corrupted clusters + --tf-log string sets the Terraform log level (default "NONE" - no logs) (default "NONE") ``` diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 5e0aa838a..f7fb67f4b 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -152,6 +152,8 @@ const ( EnvVarNoSpinner = EnvVarPrefix + "NO_SPINNER" // MiniConstellationUID is a sentinel value for the UID of a mini constellation. MiniConstellationUID = "mini" + // TerraformLogFile is the file name of the Terraform log file. + TerraformLogFile = "terraform.log" // // Kubernetes.