Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support terraform resource import #1186

Merged
merged 2 commits into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading