Skip to content

Commit

Permalink
feat: support terraform resource import (#1186)
Browse files Browse the repository at this point in the history
  • Loading branch information
liu-hm19 authored Jun 28, 2024
1 parent b1d1e55 commit 55e7e7a
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 10 deletions.
5 changes: 4 additions & 1 deletion pkg/engine/operation/graph/resource_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
49 changes: 40 additions & 9 deletions pkg/engine/runtime/terraform/terraform_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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()
Expand All @@ -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)}
}

Expand Down Expand Up @@ -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
Expand Down
71 changes: 71 additions & 0 deletions pkg/engine/runtime/terraform/tfops/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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), "/")
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down

0 comments on commit 55e7e7a

Please sign in to comment.