From 6af7182bde0e134764b5b7eb21019ead8e80f02f Mon Sep 17 00:00:00 2001 From: markliby Date: Mon, 9 Jan 2023 18:19:43 +0800 Subject: [PATCH] update tf runtime workspace in stack dir --- .../runtime/terraform/terraform_runtime.go | 108 +++++++++++------- .../terraform/terraform_runtime_test.go | 47 ++++---- pkg/engine/runtime/terraform/tfops/store.go | 83 -------------- .../runtime/terraform/tfops/store_test.go | 88 -------------- .../runtime/terraform/tfops/workspace.go | 94 ++++++++------- .../runtime/terraform/tfops/workspace_test.go | 40 +++++-- 6 files changed, 172 insertions(+), 288 deletions(-) delete mode 100644 pkg/engine/runtime/terraform/tfops/store.go delete mode 100644 pkg/engine/runtime/terraform/tfops/store_test.go diff --git a/pkg/engine/runtime/terraform/terraform_runtime.go b/pkg/engine/runtime/terraform/terraform_runtime.go index 34d94c135..678702226 100644 --- a/pkg/engine/runtime/terraform/terraform_runtime.go +++ b/pkg/engine/runtime/terraform/terraform_runtime.go @@ -2,7 +2,8 @@ package terraform import ( "context" - "fmt" + "os" + "path/filepath" "github.com/imdario/mergo" "github.com/spf13/afero" @@ -15,31 +16,19 @@ import ( var _ runtime.Runtime = &TerraformRuntime{} type TerraformRuntime struct { - tfops.WorkspaceStore + tfops.WorkSpace } func NewTerraformRuntime() (runtime.Runtime, error) { fs := afero.Afero{Fs: afero.NewOsFs()} - ws, err := tfops.GetWorkspaceStore(fs) - if err != nil { - return nil, err - } - TFRuntime := &TerraformRuntime{ws} + ws := tfops.NewWorkSpace(fs) + TFRuntime := &TerraformRuntime{*ws} return TFRuntime, nil } // Apply terraform apply resource func (t *TerraformRuntime) Apply(ctx context.Context, request *runtime.ApplyRequest) *runtime.ApplyResponse { planState := request.PlanResource - w, ok := t.Store[planState.ResourceKey()] - if !ok { - err := t.Create(ctx, planState) - if err != nil { - return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)} - } - w = t.Store[planState.ResourceKey()] - } - // terraform dry run merge state // TODO: terraform dry run apply,not only merge state if request.DryRun { @@ -56,19 +45,35 @@ func (t *TerraformRuntime) Apply(ctx context.Context, request *runtime.ApplyRequ Extensions: planState.Extensions, }, Status: nil} } - w.SetResource(planState) - if err := w.WriteHCL(); err != nil { + stackPath := request.Stack.GetPath() + tfCacheDir := filepath.Join(stackPath, "."+planState.ResourceKey()) + t.WorkSpace.SetStackDir(stackPath) + t.WorkSpace.SetCacheDir(tfCacheDir) + t.WorkSpace.SetResource(planState) + + if err := t.WorkSpace.WriteHCL(); err != nil { return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)} } - tfstate, err := w.Apply(ctx) + _, err := os.Stat(filepath.Join(tfCacheDir, tfops.HCLLOCKFILE)) + if err != nil { + if os.IsNotExist(err) { + if err := t.WorkSpace.InitWorkSpace(ctx); err != nil { + return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)} + } + } else { + return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)} + } + } + + tfstate, err := t.WorkSpace.Apply(ctx) if err != nil { return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)} } // get terraform provider version - providerAddr, err := w.GetProvider() + providerAddr, err := t.WorkSpace.GetProvider() if err != nil { return &runtime.ApplyResponse{Resource: nil, Status: status.NewErrorStatus(err)} } @@ -90,24 +95,42 @@ func (t *TerraformRuntime) Apply(ctx context.Context, request *runtime.ApplyRequ // Read terraform show state func (t *TerraformRuntime) Read(ctx context.Context, request *runtime.ReadRequest) *runtime.ReadResponse { priorState := request.PriorResource - planState := request.PlanResource + requestResource := request.PlanResource + + // When the operation is delete, the planResource is empty, + if requestResource == nil { + requestResource = request.PriorResource + } if priorState == nil { return &runtime.ReadResponse{Resource: nil, Status: nil} } var tfstate *tfops.TFState - w, ok := t.Store[planState.ResourceKey()] - if !ok { - err := t.Create(ctx, planState) - if err != nil { - return &runtime.ReadResponse{Resource: nil, Status: status.NewErrorStatus(err)} - } - w = t.Store[priorState.ResourceKey()] - if err := w.WriteTFState(priorState); err != nil { + + stackPath := request.Stack.GetPath() + tfCacheDir := filepath.Join(stackPath, "."+requestResource.ResourceKey()) + t.WorkSpace.SetStackDir(stackPath) + t.WorkSpace.SetCacheDir(tfCacheDir) + t.WorkSpace.SetResource(requestResource) + if err := t.WorkSpace.WriteHCL(); err != nil { + return &runtime.ReadResponse{Resource: nil, Status: status.NewErrorStatus(err)} + } + _, err := os.Stat(filepath.Join(tfCacheDir, tfops.HCLLOCKFILE)) + if err != nil { + if os.IsNotExist(err) { + if err := t.WorkSpace.InitWorkSpace(ctx); err != nil { + return &runtime.ReadResponse{Resource: nil, Status: status.NewErrorStatus(err)} + } + } else { return &runtime.ReadResponse{Resource: nil, Status: status.NewErrorStatus(err)} } } - tfstate, err := w.RefreshOnly(ctx) + // priorState overwirte tfstate in workspace + if err := t.WorkSpace.WriteTFState(priorState); err != nil { + return &runtime.ReadResponse{Resource: nil, Status: status.NewErrorStatus(err)} + } + + tfstate, err = t.WorkSpace.RefreshOnly(ctx) if err != nil { return &runtime.ReadResponse{Resource: nil, Status: status.NewErrorStatus(err)} } @@ -116,7 +139,7 @@ func (t *TerraformRuntime) Read(ctx context.Context, request *runtime.ReadReques } // get terraform provider addr - providerAddr, err := w.GetProvider() + providerAddr, err := t.WorkSpace.GetProvider() if err != nil { return &runtime.ReadResponse{Resource: nil, Status: status.NewErrorStatus(err)} } @@ -124,11 +147,11 @@ func (t *TerraformRuntime) Read(ctx context.Context, request *runtime.ReadReques r := tfops.ConvertTFState(tfstate, providerAddr) return &runtime.ReadResponse{ Resource: &models.Resource{ - ID: planState.ID, - Type: planState.Type, + ID: requestResource.ID, + Type: requestResource.Type, Attributes: r.Attributes, - DependsOn: planState.DependsOn, - Extensions: planState.Extensions, + DependsOn: requestResource.DependsOn, + Extensions: requestResource.Extensions, }, Status: nil, } @@ -136,17 +159,16 @@ func (t *TerraformRuntime) Read(ctx context.Context, request *runtime.ReadReques // Delete terraform resource and remove workspace func (t *TerraformRuntime) Delete(ctx context.Context, request *runtime.DeleteRequest) *runtime.DeleteResponse { - w, ok := t.Store[request.Resource.ResourceKey()] - if !ok { - return &runtime.DeleteResponse{Status: status.NewErrorStatus(fmt.Errorf("%s terraform workspace not exist, cannot delete", request.Resource.ResourceKey()))} - } - if err := w.Destroy(ctx); err != nil { + stackPath := request.Stack.GetPath() + tfCacheDir := filepath.Join(stackPath, "."+request.Resource.ResourceKey()) + defer os.RemoveAll(tfCacheDir) + t.WorkSpace.SetStackDir(stackPath) + t.WorkSpace.SetCacheDir(tfCacheDir) + t.WorkSpace.SetResource(request.Resource) + if err := t.WorkSpace.Destroy(ctx); err != nil { return &runtime.DeleteResponse{Status: status.NewErrorStatus(err)} } - if err := t.Remove(ctx, request.Resource); err != nil { - return &runtime.DeleteResponse{Status: status.NewErrorStatus(err)} - } return &runtime.DeleteResponse{Status: nil} } diff --git a/pkg/engine/runtime/terraform/terraform_runtime_test.go b/pkg/engine/runtime/terraform/terraform_runtime_test.go index 9b8207ef0..adfc3d9bb 100644 --- a/pkg/engine/runtime/terraform/terraform_runtime_test.go +++ b/pkg/engine/runtime/terraform/terraform_runtime_test.go @@ -3,48 +3,55 @@ package terraform import ( "context" "os" - "path/filepath" "testing" + "github.com/spf13/afero" "github.com/stretchr/testify/assert" "kusionstack.io/kusion/pkg/engine/models" "kusionstack.io/kusion/pkg/engine/runtime" "kusionstack.io/kusion/pkg/engine/runtime/terraform/tfops" + "kusionstack.io/kusion/pkg/projectstack" ) -var testResource = models.Resource{ - ID: "example", - Type: "Terraform", - Attributes: map[string]interface{}{ - "content": "kusion", - "filename": "test.txt", - }, - Extensions: map[string]interface{}{ - "provider": "registry.terraform.io/hashicorp/local/2.2.3", - "resourceType": "local_file", - }, -} +var ( + testResource = models.Resource{ + ID: "example", + Type: "Terraform", + Attributes: map[string]interface{}{ + "content": "kusion", + "filename": "test.txt", + }, + Extensions: map[string]interface{}{ + "provider": "registry.terraform.io/hashicorp/local/2.2.3", + "resourceType": "local_file", + }, + } + stack = &projectstack.Stack{ + StackConfiguration: projectstack.StackConfiguration{Name: "fakeStack"}, + Path: "fakePath", + } +) func TestTerraformRuntime(t *testing.T) { - wd, _ := tfops.GetWorkSpaceDir() - defer os.RemoveAll(filepath.Join(wd, testResource.ID)) - tfRuntime, _ := NewTerraformRuntime() + defer os.RemoveAll(stack.GetPath()) + tfRuntime := TerraformRuntime{*tfops.NewWorkSpace(afero.Afero{Fs: afero.NewOsFs()})} + t.Run("ApplyDryRun", func(t *testing.T) { - response := tfRuntime.Apply(context.TODO(), &runtime.ApplyRequest{PlanResource: &testResource, DryRun: true}) + response := tfRuntime.Apply(context.TODO(), &runtime.ApplyRequest{PlanResource: &testResource, DryRun: true, Stack: stack}) assert.Equalf(t, nil, response.Status, "Execute(%v)", "Apply") }) t.Run("Apply", func(t *testing.T) { - response := tfRuntime.Apply(context.TODO(), &runtime.ApplyRequest{PlanResource: &testResource, DryRun: false}) + response := tfRuntime.Apply(context.TODO(), &runtime.ApplyRequest{PlanResource: &testResource, DryRun: false, Stack: stack}) assert.Equalf(t, nil, response.Status, "Execute(%v)", "Apply") }) t.Run("Read", func(t *testing.T) { - response := tfRuntime.Read(context.TODO(), &runtime.ReadRequest{PlanResource: &testResource}) + response := tfRuntime.Read(context.TODO(), &runtime.ReadRequest{PlanResource: &testResource, Stack: stack}) assert.Equalf(t, nil, response.Status, "Execute(%v)", "Read") }) t.Run("Delete", func(t *testing.T) { - response := tfRuntime.Delete(context.TODO(), &runtime.DeleteRequest{Resource: &testResource}) + response := tfRuntime.Delete(context.TODO(), &runtime.DeleteRequest{Resource: &testResource, Stack: stack}) assert.Equalf(t, nil, response.Status, "Execute(%v)", "Delete") }) } diff --git a/pkg/engine/runtime/terraform/tfops/store.go b/pkg/engine/runtime/terraform/tfops/store.go deleted file mode 100644 index afcd1c5a9..000000000 --- a/pkg/engine/runtime/terraform/tfops/store.go +++ /dev/null @@ -1,83 +0,0 @@ -package tfops - -import ( - "context" - "fmt" - "os" - "path/filepath" - - "github.com/spf13/afero" - "kusionstack.io/kusion/pkg/engine/models" -) - -// WorkspaceStore store Terraform workspaces. -type WorkspaceStore struct { - Store map[string]*WorkSpace - Fs afero.Afero -} - -// Create make Terraform workspace for given resources. -// convert kusion resource to hcl json and write to file -// and init in the workspace folder -func (ws *WorkspaceStore) Create(ctx context.Context, resource *models.Resource) error { - w, ok := ws.Store[resource.ResourceKey()] - if !ok { - ws.Store[resource.ResourceKey()] = NewWorkSpace(resource, ws.Fs) - w = ws.Store[resource.ResourceKey()] - } - // write hcl json to file - if err := w.WriteHCL(); err != nil { - return fmt.Errorf("write hcl error: %v", err) - } - - // init workspace - if err := w.InitWorkSpace(ctx); err != nil { - return fmt.Errorf("init workspace error: %v", err) - } - return nil -} - -// Remove delete workspace directory and delete its record from the store. -func (ws *WorkspaceStore) Remove(ctx context.Context, resource *models.Resource) error { - w, ok := ws.Store[resource.ResourceKey()] - if !ok { - return nil - } - if err := w.fs.RemoveAll(w.dir); err != nil { - return fmt.Errorf("remove workspace error %v", err) - } - delete(ws.Store, resource.ResourceKey()) - return nil -} - -// GetWorkspaceStore find directory in the filesystem and store workspace -// return all terraform workspace record in the filesystem. -func GetWorkspaceStore(fs afero.Afero) (WorkspaceStore, error) { - ws := WorkspaceStore{ - Store: make(map[string]*WorkSpace), - Fs: fs, - } - wd, _ := GetWorkSpaceDir() - _, err := fs.Stat(wd) - if err != nil { - if os.IsNotExist(err) { - if err = fs.MkdirAll(wd, os.ModePerm); err != nil { - return ws, err - } - } else { - return ws, err - } - } - dirs, err := afero.ReadDir(fs, wd) - if err != nil { - return ws, err - } - for _, dir := range dirs { - workspace := WorkSpace{ - fs: fs, - dir: filepath.Join(wd, dir.Name()), - } - ws.Store[dir.Name()] = &workspace - } - return ws, nil -} diff --git a/pkg/engine/runtime/terraform/tfops/store_test.go b/pkg/engine/runtime/terraform/tfops/store_test.go deleted file mode 100644 index b63116f50..000000000 --- a/pkg/engine/runtime/terraform/tfops/store_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package tfops - -import ( - "context" - "fmt" - "testing" -) - -func TestCreate(t *testing.T) { - type args struct { - ws *WorkspaceStore - } - - tests := map[string]struct { - args - }{ - "Success": { - args: args{ - ws: &WorkspaceStore{ - Store: make(map[string]*WorkSpace), - Fs: fs, - }, - }, - }, - } - - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - if err := tt.args.ws.Create(context.TODO(), &resourceTest); err != nil { - t.Errorf("\n workspaceStore Create error: %v", err) - } - }) - } -} - -func TestRemove(t *testing.T) { - type args struct { - ws *WorkspaceStore - } - - tests := map[string]struct { - args - }{ - "SuccessRemove": { - args: args{ - ws: &WorkspaceStore{ - Store: make(map[string]*WorkSpace), - Fs: fs, - }, - }, - }, - } - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - if err := tt.args.ws.Remove(context.TODO(), &resourceTest); err != nil { - t.Errorf("\n workspaceStore Remove error: %v", err) - } - }) - } -} - -func TestGetWorkspaceStore(t *testing.T) { - type args struct { - ws *WorkspaceStore - } - - tests := map[string]struct { - args - }{ - "GetworkspaceStore": { - args: args{ - &WorkspaceStore{ - Store: make(map[string]*WorkSpace), - Fs: fs, - }, - }, - }, - } - for name, tt := range tests { - t.Run(name, func(t *testing.T) { - ws, err := GetWorkspaceStore(tt.ws.Fs) - fmt.Println(ws) - if err != nil { - t.Errorf("\nGetWorkspaceStore error: %v", err) - } - }) - } -} diff --git a/pkg/engine/runtime/terraform/tfops/workspace.go b/pkg/engine/runtime/terraform/tfops/workspace.go index ea4c9d307..f4a8076a6 100644 --- a/pkg/engine/runtime/terraform/tfops/workspace.go +++ b/pkg/engine/runtime/terraform/tfops/workspace.go @@ -17,7 +17,6 @@ import ( "kusionstack.io/kusion/pkg/engine/models" "kusionstack.io/kusion/pkg/log" - "kusionstack.io/kusion/pkg/util/kfile" ) const ( @@ -27,9 +26,10 @@ const ( ) type WorkSpace struct { - resource *models.Resource - fs afero.Afero - dir string + resource *models.Resource + fs afero.Afero + stackDir string + tfCacheDir string } // SetResource set workspace resource @@ -37,23 +37,25 @@ func (w *WorkSpace) SetResource(resource *models.Resource) { w.resource = resource } -func NewWorkSpace(resource *models.Resource, fs afero.Afero) *WorkSpace { - wd, _ := GetWorkSpaceDir() - return &WorkSpace{ - resource: resource, - fs: fs, - dir: filepath.Join(wd, resource.ResourceKey()), - } +// SetFS set filesystem +func (w *WorkSpace) SetFS(fs afero.Afero) { + w.fs = fs } -// GetWrokSpaceDir return kusion terrafrom runtime workspace dir -// Defalut workspace dir is ~/.kusion/.terraform -func GetWorkSpaceDir() (string, error) { - kusionDir, err := kfile.KusionDataFolder() - if err != nil { - return "", err +// SetStackDir set workspace work directory. +func (w *WorkSpace) SetStackDir(stackDir string) { + w.stackDir = stackDir +} + +// SetCacheDir set tf cache work directory. +func (w *WorkSpace) SetCacheDir(cacheDir string) { + w.tfCacheDir = cacheDir +} + +func NewWorkSpace(fs afero.Afero) *WorkSpace { + return &WorkSpace{ + fs: fs, } - return filepath.Join(kusionDir, ".terraform"), nil } // WriteHCL convert kusion Resource to HCL json @@ -86,18 +88,18 @@ func (w *WorkSpace) WriteHCL() error { return fmt.Errorf("marshal hcl main error: %v", err) } - _, err = w.fs.Stat(w.dir) + _, err = w.fs.Stat(w.tfCacheDir) if err != nil { if os.IsNotExist(err) { - if err := w.fs.MkdirAll(w.dir, os.ModePerm); err != nil { + if err := w.fs.MkdirAll(w.tfCacheDir, os.ModePerm); err != nil { return fmt.Errorf("create workspace error: %v", err) } } else { return err } } - err = w.fs.WriteFile(filepath.Join(w.dir, HCLMAINFILE), hclMain, 0o600) + err = w.fs.WriteFile(filepath.Join(w.tfCacheDir, HCLMAINFILE), hclMain, 0o600) if err != nil { return fmt.Errorf("write hcl main.tf.json error: %v", err) } @@ -129,7 +131,7 @@ func (w *WorkSpace) WriteTFState(priorState *models.Resource) error { return fmt.Errorf("marshal hcl state error: %v", err) } - err = w.fs.WriteFile(filepath.Join(w.dir, TFSTATEFILE), hclState, os.ModePerm) + err = w.fs.WriteFile(filepath.Join(w.tfCacheDir, TFSTATEFILE), hclState, os.ModePerm) if err != nil { return fmt.Errorf("write hcl error: %v", err) } @@ -138,8 +140,9 @@ func (w *WorkSpace) WriteTFState(priorState *models.Resource) error { // InitWorkSpace init terraform runtime workspace func (w *WorkSpace) InitWorkSpace(ctx context.Context) error { - cmd := exec.CommandContext(ctx, "terraform", "init") - cmd.Dir = w.dir + chdir := fmt.Sprintf("-chdir=%s", w.tfCacheDir) + cmd := exec.CommandContext(ctx, "terraform", chdir, "init") + cmd.Dir = w.stackDir _, err := cmd.Output() if e, ok := err.(*exec.ExitError); ok { return errors.New(string(e.Stderr)) @@ -149,13 +152,14 @@ func (w *WorkSpace) InitWorkSpace(ctx context.Context) error { // Apply with the terraform cli apply command func (w *WorkSpace) Apply(ctx context.Context) (*TFState, error) { - err := w.CleanAndInitWorkspace(ctx) + chdir := fmt.Sprintf("-chdir=%s", w.tfCacheDir) + err := w.CleanAndInitWorkspace(ctx, chdir) if err != nil { return nil, err } - cmd := exec.CommandContext(ctx, "terraform", "apply", "-auto-approve", "-json", "-lock=false") - cmd.Dir = w.dir + cmd := exec.CommandContext(ctx, "terraform", chdir, "apply", "-auto-approve", "-json", "-lock=false") + cmd.Dir = w.stackDir out, err := cmd.CombinedOutput() if err != nil { return nil, TFError(out) @@ -170,15 +174,16 @@ func (w *WorkSpace) Apply(ctx context.Context) (*TFState, error) { // Read make terraform show call. Return terraform state model // TODO: terraform show livestate. func (w *WorkSpace) Read(ctx context.Context) (*TFState, error) { - _, err := w.fs.Stat(filepath.Join(w.dir, "terraform.tfstate")) + _, err := w.fs.Stat(filepath.Join(w.tfCacheDir, "terraform.tfstate")) if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, err } - cmd := exec.CommandContext(ctx, "terraform", "show", "-json") - cmd.Dir = w.dir + chdir := fmt.Sprintf("-chdir=%s", w.tfCacheDir) + cmd := exec.CommandContext(ctx, "terraform", chdir, "show", "-json") + cmd.Dir = w.stackDir out, err := cmd.CombinedOutput() if err != nil { return nil, TFError(out) @@ -192,12 +197,13 @@ func (w *WorkSpace) Read(ctx context.Context) (*TFState, error) { // Refresh Sync Terraform State func (w *WorkSpace) RefreshOnly(ctx context.Context) (*TFState, error) { - err := w.CleanAndInitWorkspace(ctx) + chdir := fmt.Sprintf("-chdir=%s", w.tfCacheDir) + err := w.CleanAndInitWorkspace(ctx, chdir) if err != nil { return nil, err } - cmd := exec.CommandContext(ctx, "terraform", "apply", "-auto-approve", "-json", "--refresh-only", "-lock=false") - cmd.Dir = w.dir + cmd := exec.CommandContext(ctx, "terraform", chdir, "apply", "-auto-approve", "-json", "--refresh-only", "-lock=false") + cmd.Dir = w.stackDir out, err := cmd.CombinedOutput() if err != nil { return nil, TFError(out) @@ -211,8 +217,9 @@ func (w *WorkSpace) RefreshOnly(ctx context.Context) (*TFState, error) { // Destroy make terraform destroy call. func (w *WorkSpace) Destroy(ctx context.Context) error { - cmd := exec.CommandContext(ctx, "terraform", "destroy", "-auto-approve") - cmd.Dir = w.dir + chdir := fmt.Sprintf("-chdir=%s", w.tfCacheDir) + cmd := exec.CommandContext(ctx, "terraform", chdir, "destroy", "-auto-approve") + cmd.Dir = w.stackDir out, err := cmd.CombinedOutput() if err != nil { return TFError(out) @@ -225,7 +232,7 @@ func (w *WorkSpace) Destroy(ctx context.Context) error { // eg. registry.terraform.io/hashicorp/local/2.2.3 func (w *WorkSpace) GetProvider() (string, error) { parser := hclparse.NewParser() - hclFile, diags := parser.ParseHCLFile(filepath.Join(w.dir, ".terraform.lock.hcl")) + hclFile, diags := parser.ParseHCLFile(filepath.Join(w.tfCacheDir, ".terraform.lock.hcl")) if diags != nil { return "", errors.New(diags.Error()) } @@ -264,8 +271,8 @@ func (w *WorkSpace) GetProvider() (string, error) { // CleanAndInitWorkspace will clean up the provider cache and reinitialize the workspace // when the provider version or hash is updated. -func (w *WorkSpace) CleanAndInitWorkspace(ctx context.Context) error { - isHashUpdate := w.checkHashUpdate(ctx) +func (w *WorkSpace) CleanAndInitWorkspace(ctx context.Context, chdir string) error { + isHashUpdate := w.checkHashUpdate(ctx, chdir) isVersionUpdate, err := w.checkVersionUpdate(ctx) if err != nil { return fmt.Errorf("check provider version failed: %v", err) @@ -274,8 +281,8 @@ func (w *WorkSpace) CleanAndInitWorkspace(ctx context.Context) error { // If the provider hash or version changes, delete the tf cache and reinitialize. if isHashUpdate || isVersionUpdate { log.Info("provider hash or version change.") - os.Remove(filepath.Join(w.dir, ".terraform.lock.hcl")) - os.Remove(filepath.Join(w.dir, ".terraform")) + os.Remove(filepath.Join(w.tfCacheDir, ".terraform.lock.hcl")) + os.Remove(filepath.Join(w.tfCacheDir, ".terraform")) err := w.InitWorkSpace(ctx) if err != nil { return fmt.Errorf("init terraform workspace failed: %v", err) @@ -285,11 +292,10 @@ func (w *WorkSpace) CleanAndInitWorkspace(ctx context.Context) error { } // checkHashUpdate checks whether the provider hash has changed, and returns true if changed -func (w *WorkSpace) checkHashUpdate(ctx context.Context) bool { - cmd := exec.CommandContext(ctx, "terraform", "providers", "lock") - cmd.Dir = w.dir +func (w *WorkSpace) checkHashUpdate(ctx context.Context, chdir string) bool { + cmd := exec.CommandContext(ctx, "terraform", chdir, "providers", "lock") + cmd.Dir = w.stackDir output, _ := cmd.Output() - return strings.Contains(string(output), "Terraform has updated the lock file") } diff --git a/pkg/engine/runtime/terraform/tfops/workspace_test.go b/pkg/engine/runtime/terraform/tfops/workspace_test.go index 76f7a1f0d..aeacb6508 100644 --- a/pkg/engine/runtime/terraform/tfops/workspace_test.go +++ b/pkg/engine/runtime/terraform/tfops/workspace_test.go @@ -2,6 +2,7 @@ package tfops import ( "context" + "os" "path/filepath" "testing" @@ -75,7 +76,7 @@ func TestWriteHCL(t *testing.T) { }{ "writeSuccess": { args: args{ - w: NewWorkSpace(&resourceTest, fs), + w: NewWorkSpace(fs), }, want: want{ maintf: "{\"provider\":{\"local\":null},\"resource\":{\"local_file\":{\"kusion_example\":{\"content\":\"kusion\",\"filename\":\"test.txt\"}}},\"terraform\":{\"required_providers\":{\"local\":{\"source\":\"registry.terraform.io/hashicorp/local\",\"version\":\"2.2.3\"}}}}", @@ -85,11 +86,13 @@ func TestWriteHCL(t *testing.T) { for name, tt := range cases { t.Run(name, func(t *testing.T) { + tt.args.w.SetResource(&resourceTest) + tt.args.w.SetCacheDir(".test") if err := tt.args.w.WriteHCL(); err != nil { t.Errorf("writeHCL error: %v", err) } - s, _ := fs.ReadFile(filepath.Join(tt.w.dir, "main.tf.json")) + s, _ := fs.ReadFile(filepath.Join(tt.w.tfCacheDir, "main.tf.json")) if diff := cmp.Diff(string(s), tt.want.maintf); diff != "" { t.Errorf("\n%s\nWriteHCL(...): -want maintf, +got maintf:\n%s", name, diff) } @@ -112,7 +115,7 @@ func TestWriteTFState(t *testing.T) { }{ "writeSuccess": { args: args{ - w: NewWorkSpace(&resourceTest, fs), + w: NewWorkSpace(fs), }, want: want{ tfstate: "{\"resources\":[{\"instances\":[{\"attributes\":{\"content\":\"kusion\",\"filename\":\"test.txt\"}}],\"mode\":\"managed\",\"name\":\"kusion_example\",\"provider\":\"provider[\\\"registry.terraform.io/hashicorp/local\\\"]\",\"type\":\"local_file\"}],\"version\":4}", @@ -122,11 +125,13 @@ func TestWriteTFState(t *testing.T) { for name, tt := range cases { t.Run(name, func(t *testing.T) { + tt.args.w.SetResource(&resourceTest) + tt.args.w.SetCacheDir(".test") if err := tt.args.w.WriteTFState(&resourceTest); err != nil { t.Errorf("WriteTFState error: %v", err) } - s, _ := fs.ReadFile(filepath.Join(tt.w.dir, "terraform.tfstate")) + s, _ := fs.ReadFile(filepath.Join(tt.w.tfCacheDir, "terraform.tfstate")) if diff := cmp.Diff(string(s), tt.want.tfstate); diff != "" { t.Errorf("\n%s\nWriteTFState(...): -want tfstate, +got tfstate:\n%s", name, diff) } @@ -149,12 +154,14 @@ func TestInitWorkspace(t *testing.T) { }{ "initws": { args: args{ - w: NewWorkSpace(&resourceTest, fs), + w: NewWorkSpace(fs), }, }, } for name, tt := range cases { t.Run(name, func(t *testing.T) { + tt.args.w.SetResource(&resourceTest) + tt.args.w.SetCacheDir(".test") err := tt.args.w.InitWorkSpace(context.TODO()) if diff := cmp.Diff(tt.want.err, err); diff != "" { t.Errorf("\nInitWorkSpace(...) -want err, +got err: \n%s", diff) @@ -173,12 +180,15 @@ func TestApply(t *testing.T) { }{ "applySuccess": { args: args{ - w: NewWorkSpace(&resourceTest, fs), + w: NewWorkSpace(fs), }, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { + tt.w.SetResource(&resourceTest) + tt.w.SetCacheDir(".test") + tt.args.w.SetStackDir(".") if err := tt.w.WriteHCL(); err != nil { t.Errorf("\nWriteHCL error: %v", err) } @@ -201,12 +211,15 @@ func TestRead(t *testing.T) { }{ "readSuccess": { args: args{ - w: NewWorkSpace(&resourceTest, fs), + w: NewWorkSpace(fs), }, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { + tt.args.w.SetResource(&resourceTest) + tt.args.w.SetCacheDir(".test") + tt.args.w.SetStackDir(".") if _, err := tt.args.w.Read(context.TODO()); err != nil { t.Errorf("\n Read error: %v", err) } @@ -223,12 +236,15 @@ func TestRefreshOnly(t *testing.T) { }{ "readSuccess": { args: args{ - w: NewWorkSpace(&resourceTest, fs), + w: NewWorkSpace(fs), }, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { + tt.args.w.SetResource(&resourceTest) + tt.args.w.SetCacheDir(".test") + tt.args.w.SetStackDir(".") if _, err := tt.args.w.RefreshOnly(context.TODO()); err != nil { t.Errorf("\n RefreshOnly error: %v", err) } @@ -253,7 +269,7 @@ func TestGerProvider(t *testing.T) { }{ "Success": { args: args{ - w: NewWorkSpace(&resourceTest, fs), + w: NewWorkSpace(fs), }, want: want{ addr: "registry.terraform.io/hashicorp/local/2.2.3", @@ -304,6 +320,7 @@ func mockProviderAddr() { } func TestDestory(t *testing.T) { + defer os.RemoveAll(".test") type args struct { w *WorkSpace } @@ -318,7 +335,7 @@ func TestDestory(t *testing.T) { }{ "success": { args: args{ - w: NewWorkSpace(&resourceTest, fs), + w: NewWorkSpace(fs), }, want: want{ err: nil, @@ -327,6 +344,9 @@ func TestDestory(t *testing.T) { } for name, tt := range cases { t.Run(name, func(t *testing.T) { + tt.args.w.SetResource(&resourceTest) + tt.args.w.SetCacheDir(".test") + tt.args.w.SetStackDir(".") if err := tt.w.Destroy(context.TODO()); err != nil { t.Errorf("terraform destroy error: %v", err) }