Skip to content

Commit

Permalink
Move working directory creation to newTerraform
Browse files Browse the repository at this point in the history
Signed-off-by: Karishma Chawla <[email protected]>
  • Loading branch information
kachawla committed Nov 2, 2023
1 parent 0248a0d commit 2dde25c
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 145 deletions.
71 changes: 17 additions & 54 deletions pkg/recipes/terraform/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"time"

install "github.com/hashicorp/hc-install"
Expand All @@ -43,11 +40,6 @@ import (
"k8s.io/client-go/kubernetes"
)

const (
executionSubDir = "deploy"
workingDirFileMode fs.FileMode = 0700
)

var (
// ErrRecipeNameEmpty is the error when the recipe name is empty.
ErrRecipeNameEmpty = errors.New("recipe name cannot be empty")
Expand Down Expand Up @@ -90,26 +82,20 @@ func (e *executor) Deploy(ctx context.Context, options Options) (*tfjson.State,
return nil, err
}

// Create Working Directory
workingDir, err := createWorkingDir(ctx, options.RootDir)
if err != nil {
return nil, err
}

// Create a new instance of tfexec.Terraform with current the Terraform installation path
tf, err := NewTerraform(ctx, workingDir, execPath)
// Create a new instance of tfexec.Terraform with current Terraform installation path
tf, err := NewTerraform(ctx, options.RootDir, execPath)
if err != nil {
return nil, err
}

// Create Terraform config in the working directory
kubernetesBackendSuffix, err := e.generateConfig(ctx, tf, workingDir, options)
kubernetesBackendSuffix, err := e.generateConfig(ctx, tf, options)
if err != nil {
return nil, err
}

// Run TF Init and Apply in the working directory
state, err := initAndApply(ctx, tf, workingDir)
state, err := initAndApply(ctx, tf)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -145,20 +131,14 @@ func (e *executor) Delete(ctx context.Context, options Options) error {
return err
}

// Create Working Directory
workingDir, err := createWorkingDir(ctx, options.RootDir)
if err != nil {
return err
}

// Create a new instance of tfexec.Terraform with current the Terraform installation path
tf, err := NewTerraform(ctx, workingDir, execPath)
// Create a new instance of tfexec.Terraform with current Terraform installation path
tf, err := NewTerraform(ctx, options.RootDir, execPath)
if err != nil {
return err
}

// Create Terraform config in the working directory
kubernetesBackendSuffix, err := e.generateConfig(ctx, tf, workingDir, options)
kubernetesBackendSuffix, err := e.generateConfig(ctx, tf, options)
if err != nil {
return err
}
Expand Down Expand Up @@ -211,24 +191,18 @@ func (e *executor) GetRecipeMetadata(ctx context.Context, options Options) (map[
return nil, err
}

// Create Working Directory
workingDir, err := createWorkingDir(ctx, options.RootDir)
// Create a new instance of tfexec.Terraform with current Terraform installation path
tf, err := NewTerraform(ctx, options.RootDir, execPath)
if err != nil {
return nil, err
}

// Create a new instance of tfexec.Terraform with current the Terraform installation path
tf, err := NewTerraform(ctx, workingDir, execPath)
_, err = getTerraformConfig(ctx, tf.WorkingDir(), options)
if err != nil {
return nil, err
}

_, err = getTerraformConfig(ctx, workingDir, options)
if err != nil {
return nil, err
}

result, err := downloadAndInspect(ctx, tf, workingDir, options)
result, err := downloadAndInspect(ctx, tf, options)
if err != nil {
return nil, err
}
Expand All @@ -238,28 +212,17 @@ func (e *executor) GetRecipeMetadata(ctx context.Context, options Options) (map[
}, nil
}

func createWorkingDir(ctx context.Context, tfDir string) (string, error) {
logger := ucplog.FromContextOrDiscard(ctx)

workingDir := filepath.Join(tfDir, executionSubDir)
logger.Info(fmt.Sprintf("Creating Terraform working directory: %q", workingDir))
if err := os.MkdirAll(workingDir, workingDirFileMode); err != nil {
return "", fmt.Errorf("failed to create working directory for terraform execution: %w", err)
}

return workingDir, nil
}

// generateConfig generates Terraform configuration with required inputs for the module, providers and backend to be initialized and applied.
func (e *executor) generateConfig(ctx context.Context, tf *tfexec.Terraform, workingDir string, options Options) (string, error) {
func (e *executor) generateConfig(ctx context.Context, tf *tfexec.Terraform, options Options) (string, error) {
logger := ucplog.FromContextOrDiscard(ctx)
workingDir := tf.WorkingDir()

tfConfig, err := getTerraformConfig(ctx, workingDir, options)
if err != nil {
return "", err
}

loadedModule, err := downloadAndInspect(ctx, tf, workingDir, options)
loadedModule, err := downloadAndInspect(ctx, tf, options)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -317,7 +280,7 @@ func (e *executor) generateConfig(ctx context.Context, tf *tfexec.Terraform, wor
}

// downloadAndInspect handles downloading the TF module and retrieving the necessary information
func downloadAndInspect(ctx context.Context, tf *tfexec.Terraform, workingDir string, options Options) (*moduleInspectResult, error) {
func downloadAndInspect(ctx context.Context, tf *tfexec.Terraform, options Options) (*moduleInspectResult, error) {
logger := ucplog.FromContextOrDiscard(ctx)

// Download the Terraform module to the working directory.
Expand All @@ -336,7 +299,7 @@ func downloadAndInspect(ctx context.Context, tf *tfexec.Terraform, workingDir st
// Load the downloaded module to retrieve providers and variables required by the module.
// This is needed to add the appropriate providers config and populate the value of recipe context variable.
logger.Info(fmt.Sprintf("Inspecting the downloaded Terraform module: %s", options.EnvRecipe.TemplatePath))
loadedModule, err := inspectModule(workingDir, options.EnvRecipe.Name)
loadedModule, err := inspectModule(tf.WorkingDir(), options.EnvRecipe.Name)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -369,7 +332,7 @@ func getTerraformConfig(ctx context.Context, workingDir string, options Options)
}

// initAndApply runs Terraform init and apply in the provided working directory.
func initAndApply(ctx context.Context, tf *tfexec.Terraform, workingDir string) (*tfjson.State, error) {
func initAndApply(ctx context.Context, tf *tfexec.Terraform) (*tfjson.State, error) {
logger := ucplog.FromContextOrDiscard(ctx)

// Initialize Terraform
Expand Down
60 changes: 7 additions & 53 deletions pkg/recipes/terraform/execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ limitations under the License.
package terraform

import (
"os"
"path/filepath"
"testing"

Expand All @@ -28,36 +27,6 @@ import (
"github.com/stretchr/testify/require"
)

func TestCreateWorkingDir_Created(t *testing.T) {
// Create a temporary directory for testing.
testDir := t.TempDir()

expectedWorkingDir := filepath.Join(testDir, executionSubDir)
workingDir, err := createWorkingDir(testcontext.New(t), testDir)
require.NoError(t, err)
require.Equal(t, expectedWorkingDir, workingDir)

// Assert that the working directory was created.
_, err = os.Stat(workingDir)
require.NoError(t, err)
}

func TestCreateWorkingDir_Error(t *testing.T) {
// Create a temporary directory for testing.
testDir := t.TempDir()
// Create a read-only directory within the temporary directory.
readOnlyDir := filepath.Join(testDir, "read-only-dir")
err := os.MkdirAll(readOnlyDir, 0555)
require.NoError(t, err)

// Call createWorkingDir with the read-only directory.
_, err = createWorkingDir(testcontext.New(t), readOnlyDir)

// Assert that createWorkingDir returns an error.
require.Error(t, err)
require.Contains(t, err.Error(), "failed to create working directory")
}

func TestGenerateConfig(t *testing.T) {
configTests := []struct {
name string
Expand All @@ -73,17 +42,6 @@ func TestGenerateConfig(t *testing.T) {
},
},
err: ErrRecipeNameEmpty.Error(),
}, {
name: "invalid working dir",
workingDir: "/invalid-dir",
opts: Options{
EnvRecipe: &recipes.EnvironmentDefinition{
Name: "test-recipe",
TemplatePath: "test/module/source",
},
ResourceRecipe: &recipes.ResourceMetadata{},
},
err: "error creating file: open /invalid-dir/main.tf.json",
},
}

Expand All @@ -93,10 +51,11 @@ func TestGenerateConfig(t *testing.T) {
if tc.workingDir == "" {
tc.workingDir = t.TempDir()
}
tf, _ := tfexec.NewTerraform(tc.workingDir, filepath.Join(tc.workingDir, "terraform"))
tf, err := tfexec.NewTerraform(tc.workingDir, filepath.Join(tc.workingDir, "terraform"))
require.NoError(t, err)

e := executor{}
_, err := e.generateConfig(ctx, tf, tc.workingDir, tc.opts)
_, err = e.generateConfig(ctx, tf, tc.opts)
require.Error(t, err)
require.ErrorContains(t, err, tc.err)
})
Expand All @@ -107,8 +66,6 @@ func Test_GetTerraformConfig(t *testing.T) {
// Create a temporary directory for testing.
testDir := t.TempDir()

workingDir, err := createWorkingDir(testcontext.New(t), testDir)
require.NoError(t, err)
options := Options{
EnvRecipe: &recipes.EnvironmentDefinition{
Name: "test-recipe",
Expand All @@ -121,7 +78,7 @@ func Test_GetTerraformConfig(t *testing.T) {
Module: map[string]config.TFModuleConfig{
"test-recipe": {"source": "test/module/source"}},
}
tfConfig, err := getTerraformConfig(testcontext.New(t), workingDir, options)
tfConfig, err := getTerraformConfig(testcontext.New(t), testDir, options)
require.NoError(t, err)
require.Equal(t, &expectedConfig, tfConfig)
}
Expand All @@ -130,8 +87,6 @@ func Test_GetTerraformConfig_EmptyRecipeName(t *testing.T) {
// Create a temporary directory for testing.
testDir := t.TempDir()

workingDir, err := createWorkingDir(testcontext.New(t), testDir)
require.NoError(t, err)
options := Options{
EnvRecipe: &recipes.EnvironmentDefinition{
Name: "",
Expand All @@ -140,14 +95,13 @@ func Test_GetTerraformConfig_EmptyRecipeName(t *testing.T) {
ResourceRecipe: &recipes.ResourceMetadata{},
}

_, err = getTerraformConfig(testcontext.New(t), workingDir, options)
_, err := getTerraformConfig(testcontext.New(t), testDir, options)
require.Error(t, err)
require.Equal(t, err, ErrRecipeNameEmpty)
}

func Test_GetTerraformConfig_InvalidDirectory(t *testing.T) {
// Create a temporary directory for testing.
workingDir := "invalid directory"
workingDir := "invalid-directory"
options := Options{
EnvRecipe: &recipes.EnvironmentDefinition{
Name: "test-recipe",
Expand All @@ -158,5 +112,5 @@ func Test_GetTerraformConfig_InvalidDirectory(t *testing.T) {

_, err := getTerraformConfig(testcontext.New(t), workingDir, options)
require.Error(t, err)
require.Contains(t, err.Error(), "error creating file: open invalid directory/main.tf.json: no such file or directory")
require.Contains(t, err.Error(), "error creating file: open invalid-directory/main.tf.json: no such file or directory")
}
56 changes: 56 additions & 0 deletions pkg/recipes/terraform/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
Copyright 2023 The Radius Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package terraform

import (
"context"

"github.com/go-logr/logr"
"github.com/hashicorp/terraform-exec/tfexec"
"github.com/radius-project/radius/pkg/ucp/ucplog"
)

// tfLogWrapper is a wrapper around the Terraform logger to stream the logs to the Radius logger.
type tfLogWrapper struct {
logger logr.Logger
isStdErr bool
}

// Write implements the io.Writer interface to stream the Terraform logs to the Radius logger.
func (w *tfLogWrapper) Write(p []byte) (n int, err error) {
if w.isStdErr {
w.logger.Error(nil, string(p))
} else {
w.logger.Info(string(p))
}

return len(p), nil
}

// configureTerraformLogs configures the Terraform logs to be streamed to the Radius logs.
func configureTerraformLogs(ctx context.Context, tf *tfexec.Terraform) {
logger := ucplog.FromContextOrDiscard(ctx)

err := tf.SetLog("TRACE")
if err != nil {
logger.Error(err, "Failed to set log level for Terraform")
return
}

tf.SetStdout(&tfLogWrapper{logger: logger})
tf.SetStderr(&tfLogWrapper{logger: logger, isStdErr: true})
}
Loading

0 comments on commit 2dde25c

Please sign in to comment.