diff --git a/pkg/engine/operation/graph/resource_node.go b/pkg/engine/operation/graph/resource_node.go index 456eee03e..f6c405d32 100644 --- a/pkg/engine/operation/graph/resource_node.go +++ b/pkg/engine/operation/graph/resource_node.go @@ -320,7 +320,10 @@ func (rn *ResourceNode) applyResource(operation *models.Operation, prior, planed log.Infof("planed resource and live resource are equal") // auto import resources exist in intent and live cluster but not recorded in release file if prior == nil { - response := rt.Import(context.Background(), &runtime.ImportRequest{PlanResource: planed}) + response := rt.Import(context.Background(), &runtime.ImportRequest{ + PlanResource: planed, + Stack: operation.Stack, + }) s = response.Status log.Debugf("import resource:%s, resource:%v", planed.ID, json.Marshal2String(s)) res = response.Resource diff --git a/pkg/engine/runtime/terraform/terraform_runtime.go b/pkg/engine/runtime/terraform/terraform_runtime.go index df0a2abf7..a2450155f 100644 --- a/pkg/engine/runtime/terraform/terraform_runtime.go +++ b/pkg/engine/runtime/terraform/terraform_runtime.go @@ -181,9 +181,13 @@ func (t *TerraformRuntime) Read(ctx context.Context, request *runtime.ReadReques priorResource := request.PriorResource planResource := request.PlanResource + if priorResource == nil && planResource == nil { + return &runtime.ReadResponse{Resource: nil, Status: nil} + } + // when the operation is delete, planResource is nil, the planResource is set to priorResource, // tf runtime uses planResource to rebuild tfcache resources. - if planResource == nil && priorResource != nil { + if planResource == nil { // planResource is nil representing that this is a Delete action. // We only need to refresh the tf.state files and return the latest resources state in this method. // Most fields in the `attributes` field of resource aren't necessary for the command `terraform apply -refresh-only`. @@ -195,10 +199,13 @@ func (t *TerraformRuntime) Read(ctx context.Context, request *runtime.ReadReques DependsOn: priorResource.DependsOn, Extensions: priorResource.Extensions, } + + // For the resource to be deleted, the 'import_id' attribute in 'Extensions' field should be removed. + if _, ok := planResource.Extensions[tfops.ImportIDKey].(string); ok { + delete(planResource.Extensions, tfops.ImportIDKey) + } } - if priorResource == nil { - return &runtime.ReadResponse{Resource: nil, Status: nil} - } + var tfstate *tfops.StateRepresentation t.mu.Lock() @@ -223,8 +230,19 @@ func (t *TerraformRuntime) Read(ctx context.Context, request *runtime.ReadReques } } - // priorResource overwrite tfstate in workspace - if err = t.WorkSpace.WriteTFState(priorResource); err != nil { + if priorResource == nil { + // For resources declared with 'import_id' in the 'Extensions' field, + // use 'terraform import' to import the latest state. + importID, ok := planResource.Extensions[tfops.ImportIDKey].(string) + if ok && importID != "" { + if err = t.WorkSpace.ImportResource(ctx, importID); err != nil { + return &runtime.ReadResponse{Resource: nil, Status: v1.NewErrorStatus(err)} + } + } else { + return &runtime.ReadResponse{Resource: nil, Status: nil} + } + } else if err = t.WorkSpace.WriteTFState(priorResource); err != nil { + // priorResource overwrite tfstate in workspace return &runtime.ReadResponse{Resource: nil, Status: v1.NewErrorStatus(err)} } @@ -257,9 +275,22 @@ func (t *TerraformRuntime) Read(ctx context.Context, request *runtime.ReadReques } func (t *TerraformRuntime) Import(ctx context.Context, request *runtime.ImportRequest) *runtime.ImportResponse { - // TODO change to terraform cli import - log.Info("skip import TF resource:%s", request.PlanResource.ID) - return nil + response := t.Read(ctx, &runtime.ReadRequest{ + PlanResource: request.PlanResource, + Stack: request.Stack, + }) + + if v1.IsErr(response.Status) { + return &runtime.ImportResponse{ + Resource: nil, + Status: response.Status, + } + } + + return &runtime.ImportResponse{ + Resource: response.Resource, + Status: nil, + } } // Delete terraform resource and remove workspace diff --git a/pkg/engine/runtime/terraform/tfops/workspace.go b/pkg/engine/runtime/terraform/tfops/workspace.go index 8718115bc..5121fd477 100644 --- a/pkg/engine/runtime/terraform/tfops/workspace.go +++ b/pkg/engine/runtime/terraform/tfops/workspace.go @@ -23,6 +23,10 @@ import ( "kusionstack.io/kusion/pkg/util/kfile" ) +const ( + ImportIDKey = "kusionstack.io/import-id" +) + const ( envLog = "TF_LOG" envPluginCacheDir = "TF_PLUGIN_CACHE_DIR" @@ -101,6 +105,14 @@ func (w *WorkSpace) WriteHCL() error { }, }, } + + if importID, ok := w.resource.Extensions[ImportIDKey].(string); ok && importID != "" { + m["import"] = map[string]interface{}{ + "to": strings.Join([]string{resourceType, resourceNames[len(resourceNames)-1]}, "."), + "id": importID, + } + } + hclMain := jsonutil.Marshal2PrettyString(m) _, err := w.fs.Stat(w.tfCacheDir) @@ -121,6 +133,25 @@ func (w *WorkSpace) WriteHCL() error { return nil } +// ImportResource imports the resource state into the temporary terraform cache directory under the stack. +func (w *WorkSpace) ImportResource(ctx context.Context, id string) error { + resourceType := w.resource.Extensions["resourceType"].(string) + resourceNames := strings.Split(w.resource.ResourceKey(), ":") + if len(resourceNames) < 4 { + return fmt.Errorf("illegial resource id:%s in Intent. "+ + "Resource id format: providerNamespace:providerName:resourceType:resourceName", w.resource.ResourceKey()) + } + + to := strings.Join([]string{resourceType, resourceNames[len(resourceNames)-1]}, ".") + + // Clear the old state file before importing the latest state. + if err := w.ClearTFState(); err != nil { + return fmt.Errorf("failed to clear the old state file before importing the latest state: %v", err) + } + + return w.Import(ctx, to, id) +} + // WriteTFState writes StateRepresentation to the file, this function is for terraform apply refresh only func (w *WorkSpace) WriteTFState(priorState *v1.Resource) error { provider := strings.Split(priorState.Extensions["provider"].(string), "/") @@ -154,6 +185,22 @@ func (w *WorkSpace) WriteTFState(priorState *v1.Resource) error { return nil } +// ClearTFState clears the StateRepresentation to the file, this function is for terraform import only. +func (w *WorkSpace) ClearTFState() error { + m := map[string]interface{}{ + "version": 4, + "resources": []map[string]interface{}{}, + } + hclState := jsonutil.Marshal2PrettyString(m) + + err := w.fs.WriteFile(filepath.Join(w.tfCacheDir, tfStateFile), []byte(hclState), os.ModePerm) + if err != nil { + return fmt.Errorf("write hcl error: %v", err) + } + + return nil +} + // InitWorkSpace init terraform runtime workspace func (w *WorkSpace) InitWorkSpace(ctx context.Context) error { chdir := fmt.Sprintf("-chdir=%s", w.tfCacheDir) @@ -241,6 +288,30 @@ func (w *WorkSpace) Plan(ctx context.Context) (*PlanRepresentation, error) { return pr, err } +// Import with the terraform cli import command. +func (w *WorkSpace) Import(ctx context.Context, to, id string) error { + chdir := fmt.Sprintf("-chdir=%s", w.tfCacheDir) + err := w.CleanAndInitWorkspace(ctx) + if err != nil { + return err + } + + cmd := exec.CommandContext(ctx, "terraform", chdir, "import", "-lock=false", to, id) + cmd.Dir = w.stackDir + envs, err := w.initEnvs() + if err != nil { + return err + } + cmd.Env = envs + + out, err := cmd.CombinedOutput() + if err != nil { + return TFError(out) + } + + return nil +} + // ShowState shows local tfstate with the terraform cli show command func (w *WorkSpace) ShowState(ctx context.Context) (*StateRepresentation, error) { fi, err := w.fs.Stat(filepath.Join(w.tfCacheDir, tfStateFile))