Skip to content
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
2 changes: 1 addition & 1 deletion contracts/openapi/iac.v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1058,7 +1058,7 @@ components:
type: string
minLength: 1
maxLength: 1024
maxItems: 256
maxItems: 4096
plan_file:
type: string
description: |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ async def import_resources(
attempt = TerraformImportAttempt(
address=r.address,
resource_id=r.resource_id,
listed_id=r.listed_id,
error=result.feedback,
)
if result.ok:
Expand Down
1 change: 1 addition & 0 deletions core/src/domains/dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ class TerraformImportAttempt:

address: str
resource_id: str = field(compare=False)
listed_id: str = field(compare=False)
error: str = field(compare=False, default="")


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ async def get_import_addresses(
),
)
return {
TerraformImportAttempt(entry["address"], entry["resource_id"])
TerraformImportAttempt(
address=entry["address"],
resource_id=entry["resource_id"],
listed_id=entry["listed_id"],
)
for entry in response.result["imports"]
}

Large diffs are not rendered by default.

22 changes: 11 additions & 11 deletions core/src/infrastructure/tools/iac_import.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,6 @@
"parameters": {
"type": "object",
"properties": {
"status": {
"type": "boolean",
"description": "The final status of the inspection. `true` if at least one generated resource block was found and mapped, `false` otherwise."
},
"summary": {
"type": "string",
"description": "A detailed, human-readable narrative of the inspection: which files were inspected, which resource blocks were found, and any block that could not be mapped to a cloud resource ID together with the reason."
},
"imports": {
"type": "array",
"items": {
Expand All @@ -26,15 +18,23 @@
},
"resource_id": {
"type": "string",
"description": "The cloud provider's native resource ID to import (e.g. an Azure resource ID, AWS ARN, or GCP asset name), cased the way the Terraform provider parses it. Structural segments carry the provider's own spelling ('resourceGroups', 'Microsoft.Authorization/roleAssignments'); name segments are copied byte for byte from the listed id."
"description": "The import ID, in the exact format the \"Import\" section of the resource type's Terraform Registry documentation specifies for the provider version in use. The format depends on the provider and the resource type, for any provider: for example a name ('acme-logs' for aws_s3_bucket), a short id ('vpc-0123' for aws_vpc), a full ARN, Azure resource ID or OCID, a composite ('n/<namespace>/b/<bucket>' for oci_objectstorage_bucket), or an inventory path ('/dc1/vm/web01' for vsphere_virtual_machine). Every value in it is copied byte for byte from the listed id; only the format and the spelling of structural segments come from the documentation."
},
"listed_id": {
"type": "string",
"description": "The listed id from `<selected_resource_ids>` this import ID was built from, copied unchanged (for a previous attempt, its `listed_id`). It is kept with the attempt so that, if the import is rejected, a retry can rebuild the import ID from it."
}
},
"required": ["address", "resource_id"]
"required": ["address", "resource_id", "listed_id"]
},
"description": "The mapping of each generated resource block to the cloud resource it represents. Each entry feeds directly into `terraform import <address> <resource_id>`."
},
"summary": {
"type": "string",
"description": "A detailed, human-readable narrative of the inspection: which files were inspected, which resource blocks were found, and for each resource type the documented import format applied and the provider version it came from."
}
},
"required": ["status", "summary", "imports"]
"required": ["summary", "imports"]
}
}
]
3 changes: 1 addition & 2 deletions core/src/infrastructure/tools/tool_registry_static.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,15 +399,14 @@ def __handle_reconciliation_verdicts(
return report

def __handle_import_addresses(self, parameters: dict[str, Any]) -> dict[str, Any]:
status = parameters["status"]
summary = parameters["summary"]
imports = parameters["imports"]
if not isinstance(imports, list):
raise ToolInferenceParamsError(
message=f"IAC import inference hasn't returned the expected structure. got={imports}",
error_code=500,
)
return {"status": status, "summary": summary, "imports": imports}
return {"summary": summary, "imports": imports}

async def __handle_web_search(self, parameters: dict[str, Any]) -> dict[str, Any]:
query = parameters["query"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ def _drift_dto(in_sync: bool, plan: str = "plan output") -> TerraformDriftDTO:
def _attempts(pairs: list[tuple[str, str]]) -> set[TerraformImportAttempt]:
"""Build import attempts from (address, resource_id) pairs."""
return {
TerraformImportAttempt(address=address, resource_id=resource_id)
TerraformImportAttempt(
address=address, resource_id=resource_id, listed_id=resource_id
)
for address, resource_id in pairs
}

Expand All @@ -96,6 +98,7 @@ def _import_outcome(
TerraformImportAttempt(
address=address,
resource_id=resource_id,
listed_id=resource_id,
error=f"import failed for {address}",
)
for address, resource_id in failed or []
Expand Down Expand Up @@ -653,13 +656,15 @@ async def test_report_content_carries_the_per_resource_import_outcome(self):
{
"address": "azurerm_resource_group.main",
"resource_id": "res-1",
"listed_id": "res-1",
"error": "",
},
],
"failed": [
{
"address": "azurerm_storage_account.sta",
"resource_id": "res-2",
"listed_id": "res-2",
"error": "import failed for azurerm_storage_account.sta",
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ def setUp(self):
self.tool_svc = MagicMock()
self.llm_svc = AsyncMock()
self.template_svc = AsyncMock()
self.history = MagicMock()
self.service = TerraformImportAddressService(
tool_service=self.tool_svc,
llm_service=self.llm_svc,
Expand All @@ -42,13 +41,13 @@ def __answer(self, imports: list[dict[str, str]]) -> None:
name="import_addresses",
tool_call_id="call-1",
success=True,
result={"status": bool(imports), "imports": imports},
result={"summary": "", "imports": imports},
)

async def test_selected_ids_reach_the_prompt(self):
self.__answer([])

_ = await self.service.get_import_addresses(self.history, SELECTED_IDS)
_ = await self.service.get_import_addresses(SELECTED_IDS)

self.template_svc.render.assert_awaited_once_with(
PromptsLibrary.IMPORT_ADDRESSES, selected_ids=SELECTED_IDS
Expand All @@ -57,28 +56,33 @@ async def test_selected_ids_reach_the_prompt(self):
async def test_the_agent_can_look_up_canonical_id_casing(self):
self.__answer([])

_ = await self.service.get_import_addresses(self.history, SELECTED_IDS)
_ = await self.service.get_import_addresses(SELECTED_IDS)

# The ids it reports go straight to `terraform import`, and a
# listing API does not always case a resource type the way the
# provider parses it. Settling that needs the registry, so the
# The ids it reports go straight to `terraform import`, and each
# resource type documents its own import ID format, which is often
# not the listed id. Settling that needs the registry, so the
# workspace tools are not enough on their own.
contexts = self.tool_svc.get_available_tools.call_args.kwargs["contexts"]
self.assertIn(ToolContext.WORKSPACE_INSPECTION, contexts)
self.assertIn(ToolContext.EXTERNAL_INFORMATION, contexts)

async def test_reported_imports_become_address_id_pairs(self):
async def test_reported_imports_keep_the_listed_id_they_came_from(self):
listed_id = "arn:aws:ecs:eu-west-1:111122223333:service/prod/api"
self.__answer(
[
{
"address": "azurerm_storage_account.sttestdev002",
"resource_id": SELECTED_IDS[0],
"address": "aws_ecs_service.api",
"resource_id": "prod/api",
"listed_id": listed_id,
}
]
)

imports = await self.service.get_import_addresses(self.history, SELECTED_IDS)
imports = await self.service.get_import_addresses([listed_id])

# A retry rebuilds a rejected import ID from its listed id, so the
# listed id must survive next to the import ID it produced.
self.assertEqual(
[("azurerm_storage_account.sttestdev002", SELECTED_IDS[0])], imports
[("aws_ecs_service.api", "prod/api", listed_id)],
[(i.address, i.resource_id, i.listed_id) for i in imports],
)
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,15 @@ def _plan(ok: bool, feedback: str = "") -> TerraformPlanDTO:

def _imports(failed: list[str], plan: TerraformPlanDTO) -> TerraformImportDTO:
return TerraformImportDTO(
imported=[TerraformImportAttempt(address="a.ok", resource_id="id-ok")],
imported=[
TerraformImportAttempt(
address="a.ok", resource_id="id-ok", listed_id="id-ok"
)
],
failed=[
TerraformImportAttempt(address=a, resource_id=f"id-{a}", error="boom")
TerraformImportAttempt(
address=a, resource_id=f"id-{a}", listed_id=f"id-{a}", error="boom"
)
for a in failed
],
plan_result=plan,
Expand Down
15 changes: 9 additions & 6 deletions docs/src/modules/main/pages/guides/import-infrastructure.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,11 @@ empty text.
and pushes them to the session branch;
.. runs `init`, `validate` and `plan` on the session's targets;
.. has an agent read the branch diff and pair each generated block's
Terraform address with the cloud resource ID it must be imported
from, in the spelling the provider expects;
Terraform address with its listed cloud resource ID, then build the
import ID in the format the "Import" section of the provider's
documentation gives for that resource type and provider version
(the full ID, a name, a short ID or a composite, depending on the
type), looked up with `web_search` once per resource type;
.. runs `terraform import` for each pair, one address at a time, through
the IaC sidecar's `/v1/import`.
+
Expand Down Expand Up @@ -130,11 +133,11 @@ compliance audit and never set the lock.
`kv-app-dev`." Kumoss finds both among the unmanaged resources in the
subscription, writes an `azurerm_storage_account` and an
`azurerm_key_vault` block, and imports them. The storage account
imports; the key vault is rejected because the ID was paired in a
spelling the provider does not parse. The engine's error goes back to
imports; the key vault is rejected because its import ID was built in a
form the provider does not parse. The engine's error goes back to
the generator, which leaves the storage account's block untouched, the
address agent pairs the key vault again with the corrected ID, and the
next attempt imports only the key vault.
address agent rebuilds the key vault's import ID from its listed ID, and
the next attempt imports only the key vault.
The report shows both resources imported and `Succeeded`.

== Full import
Expand Down
2 changes: 1 addition & 1 deletion docs/src/modules/main/pages/reference/api/iac.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ cannot escape the workspace. `plan` writes it; `show` and `apply` read
the one a previous `plan` wrote.

|`targets`
|`plan` only. Up to 256 `-target=` filters. Omitted or empty plans the
|`plan` only. Up to 4096 `-target=` filters. Omitted or empty plans the
whole configuration.

|`address`
Expand Down
2 changes: 1 addition & 1 deletion services/iac/src/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class ValidateRequest(WorkspaceRequest):
class PlanRequest(ScopedRequest):
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")

targets: list[TargetStr] = Field(default_factory=list, max_length=256)
targets: list[TargetStr] = Field(default_factory=list, max_length=4096)
Comment thread
viodid marked this conversation as resolved.
Resolved
plan_file: PlanFileStr


Expand Down
Loading