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
This commit is contained in:
Moritz Sanft 2023-04-14 14:15:07 +02:00 committed by GitHub
parent ca1400819d
commit 1d0ee796e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 688 additions and 238 deletions

View file

@ -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",

View file

@ -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 ""
}
}

View file

@ -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
}

View file

@ -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
}