mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-05-02 14:26:23 -04:00
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:
parent
ca1400819d
commit
1d0ee796e8
30 changed files with 688 additions and 238 deletions
|
@ -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",
|
||||
|
|
75
cli/internal/terraform/logging.go
Normal file
75
cli/internal/terraform/logging.go
Normal 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 ""
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue