Repository: opentofu/opentofu
File: internal/tofu/node_resource_plan.go (Go)

Code before:
		// plan. In the destroy plan mode, import blocks are not relevant, that's why we skip resolving imports
		skipImports := importTarget.IsFromImportBlock() && !n.preDestroyRefresh
		if skipImports {
			err := importResolver.ExpandAndResolveImport(context.TODO(), importTarget, evalCtx)
			diags = diags.Append(err)
		}
	}

	// The above dealt with the expansion of the containing module, so now
	// we need to deal with the expansion of the resource itself across all
	// instances of the module.
	//
	// We'll gather up all of the leaf instances we learn about along the way
	// so that we can inform the checks subsystem of which instances it should
	// be expecting check results for, below.
	instAddrs := addrs.MakeSet[addrs.Checkable]()
	for _, module := range moduleInstances {
		resAddr := n.Addr.Resource.Absolute(module)
		err := n.expandResourceInstances(context.TODO(), evalCtx, resAddr, &g, instAddrs)
		diags = diags.Append(err)
	}
	if diags.HasErrors() {
		return nil, diags.ErrWithWarnings()
	}

	// Import target validation, when the target address contains a for_each key,
	// needs to happen after we have expanded all of the resource instances,
	// since the target addresses might include references with non-existent keys.
	importDiags := n.cliImportTargetValidation(instAddrs)
	diags = diags.Append(importDiags)
	if diags.HasErrors() {
		return nil, diags.ErrWithWarnings()
	}

	// If this is a resource that participates in custom condition checks
	// (i.e. it has preconditions or postconditions) then the check state
	// wants to know the addresses of the checkable objects so that it can
	// treat them as unknown status if we encounter an error before actually
	// visiting the checks.
	if checkState := evalCtx.Checks(); checkState.ConfigHasChecks(n.NodeAbstractResource.Addr) {
		checkState.ReportCheckableObjects(n.NodeAbstractResource.Addr, instAddrs)
	}

	addRootNodeToGraph(&g)

	return &g, diags.ErrWithWarnings()
}

// cliImportTargetValidation Checks if we have an import targets from the commandline
// and the resources at the target addresses are referenced with a key corresponding to an existing instance.
func (n *nodeExpandPlannableResource) cliImportTargetValidation(instAddrs addrs.Set[addrs.Checkable]) tfdiags.Diagnostics {

Review comment (suggestion):
Hey Ilia, first of all, thanks for doing this work! It was easy to understand what was supposed to be done and I think there are good test cases covering it.

With that being said, I would like to propose an alternate implementation where we reuse more of the existing logic:

1) 
First, we're using `postPlanValidateImports` to validate these imports when we are using `import blocks`. We could reuse that on the `context_import`, after the `walker` is done.

This is similar to what we're using at `internal/tofu/context_plan.go/planWalk` function. `postPlanValidateImports` could be renamed to `postExpansionValidateImports` since only the `importResolver` is being used, it's not using something specific from the `plan`.

2)
Besides that, you would need to add the command cli import target to the `imports` list of `importResolver`, by adding them at the `DynamicExpand` from `nodeExpandPlannableResource`.

Here's the full diff of the prototype:

``` 
diff --git a/internal/tofu/context_import.go b/internal/tofu/context_import.go
index 61ef57db31..6ccc8800e3 100644
--- a/internal/tofu/context_import.go
+++ b/internal/tofu/context_import.go
@@ -331,6 +331,13 @@ func (c *Context) Import(ctx context.Context, config *configs.Config, prevRunSta
 		return state, diags
 	}
 
+	allInsts := walker.InstanceExpander.AllInstances()
+
+	importValidateDiags := c.postPlanValidateImports(walker.ImportResolver, allInsts)
+	if importValidateDiags.HasErrors() {
+		return nil, importValidateDiags
+	}
+
 	// Data sources which could not be read during the import plan will be
 	// unknown. We need to strip those objects out so that the state can be
 	// serialized.
diff --git a/internal/tofu/node_resource_plan.go b/internal/tofu/node_resource_plan.go
index b9ad1947e6..905325813d 100644
--- a/internal/tofu/node_resource_plan.go
+++ b/internal/tofu/node_resource_plan.go
@@ -170,6 +170,15 @@ func (n *nodeExpandPlannableResource) DynamicExpand(evalCtx EvalContext) (*Graph
 		// resolve the import as it's already in the resolved form
 		// In addition, if PreDestroyRefresh is true, we know we are running as part of a refresh plan, immediately before a destroy
 		// plan. In the destroy plan mode, import blocks are not relevant, that's why we skip resolving imports
+		if importTarget.IsFromImportCommandLine() {
+			importAddress := importTarget.CommandLineImportTarget.Addr
+			importResolver.imports[importAddress.String()] = EvaluatedConfigImportTarget{
+				Config: nil,
+				Addr:   importAddress,
+				ID:     importTarget.CommandLineImportTarget.ID,
+			}
+			continue
+		}
 		skipImports := importTarget.IsFromImportBlock() && !n.preDestroyRefresh
 		if skipImports {
 			err := importResolver.ExpandAndResolveImport(context.TODO(), importTarget, evalCtx)
```

We reuse existing validations so there's less code to maintain and less branches.

Code after:
		// We add CLI import targets to the import resolver. Those targets are validated after the graph walk in Context.Import method.
		// This was added to correctly validate the existence of the targeted resource instances in case for_each key is used, either on a module or on a resource.
		if importTarget.IsFromImportCommandLine() {
			importResolver.addCLIImportTarget(importTarget)
			continue
		}
		// If we have import from import block and PreDestroyRefresh is true, we know we are running as part of a refresh plan, immediately before a destroy
		// plan. In the destroy plan mode, import blocks are not relevant, that's why we skip resolving imports
		if importTarget.IsFromImportBlock() && !n.preDestroyRefresh {
			err := importResolver.ExpandAndResolveImport(context.TODO(), importTarget, evalCtx)
			diags = diags.Append(err)
		}
	}

	// The above dealt with the expansion of the containing module, so now
	// we need to deal with the expansion of the resource itself across all
	// instances of the module.
	//
	// We'll gather up all of the leaf instances we learn about along the way
	// so that we can inform the checks subsystem of which instances it should
	// be expecting check results for, below.
	instAddrs := addrs.MakeSet[addrs.Checkable]()
	for _, module := range moduleInstances {
		resAddr := n.Addr.Resource.Absolute(module)
		err := n.expandResourceInstances(context.TODO(), evalCtx, resAddr, &g, instAddrs)
		diags = diags.Append(err)
	}
	if diags.HasErrors() {
		return nil, diags.ErrWithWarnings()
	}

	// If this is a resource that participates in custom condition checks
	// (i.e. it has preconditions or postconditions) then the check state
	// wants to know the addresses of the checkable objects so that it can
	// treat them as unknown status if we encounter an error before actually
	// visiting the checks.
	if checkState := evalCtx.Checks(); checkState.ConfigHasChecks(n.NodeAbstractResource.Addr) {
		checkState.ReportCheckableObjects(n.NodeAbstractResource.Addr, instAddrs)
	}

	addRootNodeToGraph(&g)

	return &g, diags.ErrWithWarnings()
}

// expandResourceInstances calculates the dynamic expansion for the resource
// itself in the context of a particular module instance.
//
// It has several side-effects:
//   - Adds a node to Graph g for each leaf resource instance it discovers, whether present or orphaned.
//   - Registers the expansion of the resource in the "expander" object embedded inside EvalContext ctx.