diff --git a/apis/v1beta1/workspace_types.go b/apis/v1beta1/workspace_types.go index af2d57a7..d809e8e6 100644 --- a/apis/v1beta1/workspace_types.go +++ b/apis/v1beta1/workspace_types.go @@ -147,6 +147,10 @@ type WorkspaceParameters struct { // Arguments to be included in the terraform destroy CLI command DestroyArgs []string `json:"destroyArgs,omitempty"` + + // Boolean value to indicate CLI logging of terraform execution is enabled or not + // +optional + EnableTerraformCLILogging bool `json:"enableTerraformCLILogging,omitempty"` } // WorkspaceObservation are the observable fields of a Workspace. diff --git a/docs/monolith/Configuration.md b/docs/monolith/Configuration.md index 03d7d101..b3ad464c 100644 --- a/docs/monolith/Configuration.md +++ b/docs/monolith/Configuration.md @@ -355,3 +355,25 @@ spec: At Vault side configuration is also needed to allow the write operation, see [example](https://docs.crossplane.io/knowledge-base/integrations/vault-as-secret-store/) here for inspiration. + + +## Enable Terraform CLI logs + +Terraform CLI output can be written to the container logs to assist with debugging and to view detailed information about Terraform operations. +To enable it, the `Workspace` spec has an **optional** `EnableTerraformCLILogging` field. +```yaml +apiVersion: tf.upbound.io/v1beta1 +kind: Workspace +metadata: + name: example-random-generator + annotations: + meta.upbound.io/example-id: tf/v1beta1/workspace + crossplane.io/external-name: random +spec: + forProvider: + source: Inline + enableTerraformCLILogging: true +... +``` + +- `enableTerraformCLILogging`: Specifies whether logging is enabled (`true`) or disabled (`false`). When enabled, Terraform CLI command output will be written to the container logs. Default is `false` \ No newline at end of file diff --git a/examples/workspace-enable-logging.yaml b/examples/workspace-enable-logging.yaml new file mode 100644 index 00000000..17a3b5eb --- /dev/null +++ b/examples/workspace-enable-logging.yaml @@ -0,0 +1,34 @@ +apiVersion: tf.upbound.io/v1beta1 +kind: Workspace +metadata: + name: example-random-generator + annotations: + meta.upbound.io/example-id: tf/v1beta1/workspace + # The terraform workspace will be named 'random'. If you omit this + # annotation it would be derived from metadata.name - e.g. 'example-random-generator. + crossplane.io/external-name: random +spec: + forProvider: + enableTerraformCLILogging: true + source: Inline + module: | + resource "random_id" "example_id" { + byte_length = 8 + } + resource "random_password" "password" { + length = 16 + special = true + } + // Non-sensitive Outputs are written to status.atProvider.outputs and to the connection secret. + output "random_id_hex" { + value = random_id.example_id.hex + } + // Sensitive Outputs are only written to the connection secret + output "random_password" { + value = random_password.password + sensitive = true + } + // Terraform has several other random resources, see the random provider for details + writeConnectionSecretToRef: + namespace: default + name: terraform-workspace-example-random-generator diff --git a/internal/controller/workspace/workspace.go b/internal/controller/workspace/workspace.go index e292d3f6..258df5e1 100644 --- a/internal/controller/workspace/workspace.go +++ b/internal/controller/workspace/workspace.go @@ -133,8 +133,8 @@ func Setup(mgr ctrl.Manager, o controller.Options, timeout, pollJitter time.Dura usage: resource.NewProviderConfigUsageTracker(mgr.GetClient(), &v1beta1.ProviderConfigUsage{}), logger: o.Logger, fs: fs, - terraform: func(dir string, usePluginCache bool, envs ...string) tfclient { - return terraform.Harness{Path: tfPath, Dir: dir, UsePluginCache: usePluginCache, Envs: envs} + terraform: func(dir string, usePluginCache bool, enableTerraformCLILogging bool, logger logging.Logger, envs ...string) tfclient { + return terraform.Harness{Path: tfPath, Dir: dir, UsePluginCache: usePluginCache, EnableTerraformCLILogging: enableTerraformCLILogging, Logger: logger, Envs: envs} }, } @@ -175,7 +175,7 @@ type connector struct { usage resource.Tracker logger logging.Logger fs afero.Afero - terraform func(dir string, usePluginCache bool, envs ...string) tfclient + terraform func(dir string, usePluginCache bool, enableTerraformCLILogging bool, logger logging.Logger, envs ...string) tfclient } func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { //nolint:gocyclo @@ -322,7 +322,7 @@ func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.E envs[idx] = strings.Join([]string{env.Name, runtimeVal}, "=") } - tf := c.terraform(dir, *pc.Spec.PluginCache, envs...) + tf := c.terraform(dir, *pc.Spec.PluginCache, cr.Spec.ForProvider.EnableTerraformCLILogging, l, envs...) if cr.Status.AtProvider.Checksum != "" { checksum, err := tf.GenerateChecksum(ctx) if err != nil { diff --git a/internal/controller/workspace/workspace_test.go b/internal/controller/workspace/workspace_test.go index 8856098c..75776efe 100644 --- a/internal/controller/workspace/workspace_test.go +++ b/internal/controller/workspace/workspace_test.go @@ -124,7 +124,7 @@ func TestConnect(t *testing.T) { kube client.Client usage resource.Tracker fs afero.Afero - terraform func(dir string, usePluginCache bool, envs ...string) tfclient + terraform func(dir string, usePluginCache bool, enableTerraformCLILogging bool, logger logging.Logger, envs ...string) tfclient } type args struct { @@ -216,7 +216,7 @@ func TestConnect(t *testing.T) { }, usage: resource.TrackerFn(func(_ context.Context, _ resource.Managed) error { return nil }), fs: afero.Afero{Fs: afero.NewMemMapFs()}, - terraform: func(_ string, _ bool, _ ...string) tfclient { + terraform: func(_ string, _ bool, _ bool, _ logging.Logger, _ ...string) tfclient { return &MockTf{ MockInit: func(ctx context.Context, o ...terraform.InitOption) error { return nil }, } @@ -255,7 +255,7 @@ func TestConnect(t *testing.T) { errs: map[string]error{filepath.Join(tfDir, string(uid), tfCreds): errBoom}, }, }, - terraform: func(_ string, _ bool, _ ...string) tfclient { + terraform: func(_ string, _ bool, _ bool, _ logging.Logger, _ ...string) tfclient { return &MockTf{ MockInit: func(ctx context.Context, o ...terraform.InitOption) error { return nil }, } @@ -294,7 +294,7 @@ func TestConnect(t *testing.T) { errs: map[string]error{filepath.Join(tfDir, string(uid), "subdir", tfCreds): errBoom}, }, }, - terraform: func(_ string, _ bool, _ ...string) tfclient { + terraform: func(_ string, _ bool, _ bool, _ logging.Logger, _ ...string) tfclient { return &MockTf{ MockInit: func(ctx context.Context, o ...terraform.InitOption) error { return nil }, } @@ -338,7 +338,7 @@ func TestConnect(t *testing.T) { errs: map[string]error{filepath.Join("/tmp", tfDir, string(uid), ".git-credentials"): errBoom}, }, }, - terraform: func(_ string, _ bool, _ ...string) tfclient { + terraform: func(_ string, _ bool, _ bool, _ logging.Logger, _ ...string) tfclient { return &MockTf{ MockInit: func(ctx context.Context, o ...terraform.InitOption) error { return nil }, } @@ -381,7 +381,7 @@ func TestConnect(t *testing.T) { errs: map[string]error{filepath.Join("/tmp", tfDir, string(uid)): errBoom}, }, }, - terraform: func(_ string, _ bool, _ ...string) tfclient { + terraform: func(_ string, _ bool, _ bool, _ logging.Logger, _ ...string) tfclient { return &MockTf{ MockInit: func(ctx context.Context, o ...terraform.InitOption) error { return nil }, } @@ -422,7 +422,7 @@ func TestConnect(t *testing.T) { errs: map[string]error{filepath.Join(tfDir, string(uid), tfConfig): errBoom}, }, }, - terraform: func(_ string, _ bool, _ ...string) tfclient { + terraform: func(_ string, _ bool, _ bool, _ logging.Logger, _ ...string) tfclient { return &MockTf{ MockInit: func(ctx context.Context, o ...terraform.InitOption) error { return nil }, } @@ -463,7 +463,7 @@ func TestConnect(t *testing.T) { errs: map[string]error{filepath.Join(tfDir, string(uid), "subdir", tfConfig): errBoom}, }, }, - terraform: func(_ string, _ bool, _ ...string) tfclient { + terraform: func(_ string, _ bool, _ bool, _ logging.Logger, _ ...string) tfclient { return &MockTf{ MockInit: func(ctx context.Context, o ...terraform.InitOption) error { return nil }, } @@ -499,7 +499,7 @@ func TestConnect(t *testing.T) { errs: map[string]error{filepath.Join(tfDir, string(uid), tfMain): errBoom}, }, }, - terraform: func(_ string, _ bool, _ ...string) tfclient { + terraform: func(_ string, _ bool, _ bool, _ logging.Logger, _ ...string) tfclient { return &MockTf{ MockInit: func(ctx context.Context, o ...terraform.InitOption) error { return nil }, } @@ -529,7 +529,7 @@ func TestConnect(t *testing.T) { }, usage: resource.TrackerFn(func(_ context.Context, _ resource.Managed) error { return nil }), fs: afero.Afero{Fs: afero.NewMemMapFs()}, - terraform: func(_ string, _ bool, _ ...string) tfclient { + terraform: func(_ string, _ bool, _ bool, _ logging.Logger, _ ...string) tfclient { return &MockTf{MockInit: func(_ context.Context, _ ...terraform.InitOption) error { return errBoom }} }, }, @@ -553,7 +553,7 @@ func TestConnect(t *testing.T) { }, usage: resource.TrackerFn(func(_ context.Context, _ resource.Managed) error { return nil }), fs: afero.Afero{Fs: afero.NewMemMapFs()}, - terraform: func(_ string, _ bool, _ ...string) tfclient { + terraform: func(_ string, _ bool, _ bool, _ logging.Logger, _ ...string) tfclient { return &MockTf{ MockInit: func(ctx context.Context, o ...terraform.InitOption) error { return nil }, MockWorkspace: func(_ context.Context, _ string) error { return errBoom }, @@ -579,7 +579,7 @@ func TestConnect(t *testing.T) { }, usage: resource.TrackerFn(func(_ context.Context, _ resource.Managed) error { return nil }), fs: afero.Afero{Fs: afero.NewMemMapFs()}, - terraform: func(_ string, _ bool, _ ...string) tfclient { + terraform: func(_ string, _ bool, _ bool, _ logging.Logger, _ ...string) tfclient { return &MockTf{ MockGenerateChecksum: func(ctx context.Context) (string, error) { return "", errBoom }, } @@ -613,7 +613,7 @@ func TestConnect(t *testing.T) { }, usage: resource.TrackerFn(func(_ context.Context, _ resource.Managed) error { return nil }), fs: afero.Afero{Fs: afero.NewMemMapFs()}, - terraform: func(_ string, _ bool, _ ...string) tfclient { + terraform: func(_ string, _ bool, _ bool, _ logging.Logger, _ ...string) tfclient { return &MockTf{ MockGenerateChecksum: func(ctx context.Context) (string, error) { return tfChecksum, nil }, MockWorkspace: func(_ context.Context, _ string) error { return nil }, @@ -649,7 +649,7 @@ func TestConnect(t *testing.T) { }, usage: resource.TrackerFn(func(_ context.Context, _ resource.Managed) error { return nil }), fs: afero.Afero{Fs: afero.NewMemMapFs()}, - terraform: func(_ string, _ bool, _ ...string) tfclient { + terraform: func(_ string, _ bool, _ bool, _ logging.Logger, _ ...string) tfclient { return &MockTf{ MockInit: func(ctx context.Context, o ...terraform.InitOption) error { return nil }, MockGenerateChecksum: func(ctx context.Context) (string, error) { return tfChecksum, nil }, @@ -688,7 +688,7 @@ func TestConnect(t *testing.T) { }, usage: resource.TrackerFn(func(_ context.Context, _ resource.Managed) error { return nil }), fs: afero.Afero{Fs: afero.NewMemMapFs()}, - terraform: func(_ string, _ bool, _ ...string) tfclient { + terraform: func(_ string, _ bool, _ bool, _ logging.Logger, _ ...string) tfclient { return &MockTf{ MockInit: func(ctx context.Context, o ...terraform.InitOption) error { args := terraform.InitArgsToString(o) diff --git a/internal/terraform/terraform.go b/internal/terraform/terraform.go index 89bf53ab..5f0a64fd 100644 --- a/internal/terraform/terraform.go +++ b/internal/terraform/terraform.go @@ -32,10 +32,10 @@ import ( "sort" "strconv" "strings" - "syscall" - "sync" + "syscall" + "github.com/crossplane/crossplane-runtime/pkg/logging" "github.com/pkg/errors" ) @@ -47,6 +47,7 @@ const ( errRunCommand = "shutdown while running terraform command" errSigTerm = "error sending SIGTERM to child process" errWaitTerm = "error waiting for child process to terminate" + errWriteLogs = "error writing terraform logs to stdout" tfDefault = "default" ) @@ -125,6 +126,12 @@ type Harness struct { // Whether to use the terraform plugin cache UsePluginCache bool + // Whether to enable writing Terraform CLI logs to container stdout + EnableTerraformCLILogging bool + + // Logger + Logger logging.Logger + // Environment Variables Envs []string @@ -559,8 +566,18 @@ func (h Harness) Diff(ctx context.Context, o ...Option) (bool, error) { // 0 - Succeeded, diff is empty (no changes) // 1 - Errored // 2 - Succeeded, there is a diff - _, err := runCommand(ctx, cmd) - if cmd.ProcessState.ExitCode() == 2 { + log, err := runCommand(ctx, cmd) + switch cmd.ProcessState.ExitCode() { + case 1: + ee := &exec.ExitError{} + errors.As(err, &ee) + if h.EnableTerraformCLILogging { + h.Logger.Info(string(ee.Stderr), "operation", "plan") + } + case 2: + if h.EnableTerraformCLILogging { + h.Logger.Info(string(log), "operation", "plan") + } return true, nil } return false, Classify(err) @@ -591,7 +608,23 @@ func (h Harness) Apply(ctx context.Context, o ...Option) error { defer rwmutex.RUnlock() } - _, err := runCommand(ctx, cmd) + // In case of terraform apply + // 0 - Succeeded + // Non Zero output - Errored + + log, err := runCommand(ctx, cmd) + switch cmd.ProcessState.ExitCode() { + case 0: + if h.EnableTerraformCLILogging { + h.Logger.Info(string(log), "operation", "apply") + } + default: + ee := &exec.ExitError{} + errors.As(err, &ee) + if h.EnableTerraformCLILogging { + h.Logger.Info(string(ee.Stderr), "operation", "apply") + } + } return Classify(err) } @@ -620,7 +653,23 @@ func (h Harness) Destroy(ctx context.Context, o ...Option) error { defer rwmutex.RUnlock() } - _, err := runCommand(ctx, cmd) + log, err := runCommand(ctx, cmd) + + // In case of terraform destroy + // 0 - Succeeded + // Non Zero output - Errored + switch cmd.ProcessState.ExitCode() { + case 0: + if h.EnableTerraformCLILogging { + h.Logger.Info(string(log), "operation", "destroy") + } + default: + ee := &exec.ExitError{} + errors.As(err, &ee) + if h.EnableTerraformCLILogging { + h.Logger.Info(string(ee.Stderr), "operation", "destroy") + } + } return Classify(err) } diff --git a/package/crds/tf.upbound.io_workspaces.yaml b/package/crds/tf.upbound.io_workspaces.yaml index 62cb01e3..41e352f8 100644 --- a/package/crds/tf.upbound.io_workspaces.yaml +++ b/package/crds/tf.upbound.io_workspaces.yaml @@ -83,6 +83,10 @@ spec: items: type: string type: array + enableTerraformCLILogging: + description: Boolean value to indicate CLI logging of terraform + execution is enabled or not + type: boolean entrypoint: default: "" description: Entrypoint for `terraform init` within the module