Terraform/OpenAPI Conversion Design
This document is the durable long-form design reference for converting Terraform/OpenTofu configuration plus OpenAPI documents into reviewable OpenUdon UWS package artifacts.
This page is the public conversion contract. Maintainer-only planning files may exist in local checkouts, but the published behavior and release expectations are captured here and in Release Stewardship.
Goal
M7 specified the adapter contract, M8 implemented the initial openudon convert
tf path, M9 integrated package-local OpenAPI staging and handoff artifacts, and
M10 keeps the path stewarded through release checks.
Support a command shaped like:
openudon convert tf \
--config-dir ./tf \
--openapi app=./openapi/app.yaml \
--action create \
--out ./.openudon/convert
The command should produce review scaffolding, not Terraform-compatible execution. Ambiguous or unsupported behavior must become diagnostics and TODOs. Release stewardship, unsupported behavior review, and regression coverage are tracked in release-stewardship.md.
Command Integration
M8 should add an openudon convert tf command group in
../openudon/cmd/openudon/main.go. The CLI entrypoint should only parse flags,
format user-facing errors, and route to reusable adapter logic in a new
../openudon/internal/tfconvert package.
internal/tfconvert should own the conversion contract:
- load Terraform/OpenTofu static facts through
tfconfig; - load OpenAPI operation indexes through
apitools; - apply target, action, mapping, diagnostic, redaction, and deterministic output policy;
- write draft review artifacts.
The package boundary matters because later OpenUdon package generation, tests, and quality gates should reuse the adapter without shelling out to the CLI.
Static Parser Boundary
The parser belongs in github.com/OpenUdon/tfconfig.
tfconfig should:
- load Terraform/OpenTofu configuration directories;
- support
.tf,.tofu,.tf.json,.tofu.json, and override files; - support multiple files per module;
- preserve variables, locals, outputs, providers, provider aliases, required providers, resources, data sources, lifecycle, dependencies, moved/import/ removed/check/test blocks;
- preserve
count,for_each, expressions, references, source ranges, and diagnostics; - load local modules that are already present on disk;
- report missing or remote modules as diagnostics;
- mark likely secret literals as sensitive candidates and keep public JSON from emitting raw likely-secret values;
- emit deterministic
tfconfig.static.v1output.
tfconfig must not:
- run provider plugins;
- run
tofu init; - download modules;
- initialize backends;
- load state;
- refresh, plan, or apply;
- resolve credentials.
OpenUdon must consume the tfconfig.LoadDir Go API from
github.com/OpenUdon/tfconfig. The deterministic CLI JSON projection remains
an export/debug boundary and must not be the M8 integration contract unless a
later milestone explicitly promotes it.
Multi-File, Provider, And Module Support
Real Terraform/OpenTofu projects commonly split configuration across files such
as versions.tf, providers.tf, variables.tf, main.tf, and outputs.tf.
The converter must treat all eligible files in one directory as one module.
Multiple providers and aliases must be preserved as symbolic review facts:
terraform.required_providers;- provider local names;
- source addresses and version constraints;
- provider aliases;
- resource/data-source provider references;
- module
providers = { ... }mappings.
Local module support is v1 scope. Remote or unavailable modules are diagnostics.
CLI Contract
The M8 CLI contract is:
openudon convert tf \
[--config-dir DIR] \
--openapi ID=PATH \
[--openapi ID=PATH ...] \
[--action create|update|delete|replace] \
[--target ADDRESS ...] \
[--out DIR] \
[--strict]
Flag behavior:
--config-dirdefaults to.and identifies the root Terraform/OpenTofu configuration directory passed totfconfig.LoadDir.--openapiis repeatable. Each value must beid=PATH; IDs are required, non-empty, unique, stable, and used in diagnostics and review output. Paths are local OpenAPI or Swagger documents. Missing, unreadable, malformed, or duplicate-ID inputs produce diagnostics.--actionacceptscreate,update,delete, orreplace. It is required whenever the selected Terraform facts include at least one managed resource. It is not required when the selected facts contain only data sources. M8 may reject a provided action outside the allowed set before conversion begins.--targetis repeatable. Without targets, all loaded managed resources and data sources are selected. With targets, only exact address matches are selected; unmatched targets are deterministic diagnostics.--outdefaults to./.openudon/convert.--strictexits non-zero when any strict-failure diagnostic remains.
OpenAPI Input Boundary
The OpenUdon adapter must use only the narrowed OpenAPI-owned apitools APIs:
BuildOperationInventoryto load local OpenAPI/Swagger inputs;NewOperationIndexto validate and index operation IDs from an inventory;SortedOperationSummariesto produce deterministic candidate lists;SelectOperationByHintsfor prompt-safe operation selection;AuthRequirementsForOperationandAuthRequirementsForOperationsfor symbolic credential and configuration summaries.
The adapter must not reintroduce old apitools lifecycle APIs, review package
builders, LLM clients, binding contracts, or trusted-runner concepts. OpenUdon
must keep passing openudon check-apitools-boundary, and make check should
continue to include the apitools boundary target.
OpenAPI documents are sorted by CLI ID before loading and before output. The provided ID becomes the stable document key in generated review notes, diagnostics, and operation references.
The adapter loads and indexes each OpenAPI ID independently:
- call
BuildOperationInventorywith oneInventoryDocumentwhoseNameis the CLI ID and whosePathis the local document path; - call
NewOperationIndexfor that one-document inventory; - keep indexes in a map keyed by OpenAPI ID.
Duplicate operation IDs are fatal only within the same OpenAPI ID because
NewOperationIndex indexes bare operation IDs. The same operation ID may appear
in different OpenAPI documents. Generated operation references therefore use
the namespaced shape {openapi_id, operation_id}. Candidate lists are built by
iterating OpenAPI IDs in sorted order, then each document's
SortedOperationSummaries(index.OperationIDs) output.
If implementation needs package-local OpenAPI inputs, M8 may copy or reference
them under openapi/; M7 does not require that artifact.
Target Filtering
Targets match exact adapter canonical addresses derived from tfconfig module
and object addresses. tfconfig stores Module.Address separately from each
resource or data source address, so the adapter constructs a full target address
before matching:
- root managed resource:
resource.Address; - root data source:
dataSource.Address; - child module managed resource:
module.Address + "." + resource.Address; - child module data source:
module.Address + "." + dataSource.Address.
Examples:
- root managed resource:
aws_instance.web; - root data source:
data.aws_ami.base; - child module managed resource:
module.child.example_child.main; - nested child module data source:
module.child.module.grandchild.data.example_item.selected.
Target matching is exact string equality after trimming surrounding whitespace. The adapter must not perform fuzzy matching, globbing, implied module expansion, or provider-type inference for targets. Targets select only managed resources and data sources, not module calls, provider configs, outputs, moved blocks, or imports.
Unmatched targets produce diagnostics with the original target text. Matching
targets, selected modules, resources, and data sources are emitted in
tfconfig canonical order. Diagnostics for unmatched targets are sorted by
target address.
Mapping Contract
The OpenUdon adapter consumes tfconfig.static.v1 through the tfconfig Go API
plus OpenAPI operation indexes from apitools. CLI JSON remains an export/debug
format unless a later milestone promotes it as an integration boundary.
Managed Resources
Managed resources represent potential side effects and require a global
--action when any managed resource is selected. The adapter must not infer
side effects from resource type names, OpenAPI methods, Terraform lifecycle
blocks, provider defaults, or previous state.
Action mapping:
| Action | Draft scaffold purpose |
|---|---|
create |
Create a remote object from symbolic Terraform attributes. |
update |
Update an existing remote object using symbolic identity and attributes. |
delete |
Delete an existing remote object using symbolic identity. |
replace |
Composite review scaffold with separate delete and create operation slots. |
If no confident OpenAPI operation exists, the resource still appears in draft review scaffolding with a deterministic TODO operation ID. Missing or ambiguous matches are diagnostics, not hidden assumptions.
replace is not passed to SelectOperationByHints as an operation purpose
because current apitools classifiers do not classify operations as replace.
For --action replace, the adapter builds one delete-purpose candidate slot and
one create-purpose candidate slot for the same Terraform resource. Each slot may
resolve to a namespaced OpenAPI operation reference or produce its own
deterministic TODO. Provider-specific single-operation replacement remains a
review TODO unless a later milestone adds an explicit mapping input.
Data Sources
Data sources map only to read/list candidate operations. They never require
--action and must not produce create, update, delete, or replace scaffolds.
If a data source cannot be mapped confidently to a read/list operation, it appears as review scaffolding with an unresolved operation TODO. The adapter must preserve the data source address, provider reference, symbolic arguments, and source range so a reviewer can decide whether to keep, rewrite, or remove the scaffold.
Provider Bindings
Provider config addresses become symbolic OpenUdon binding names. The binding
name is normalized from the tfconfig provider address:
provider.aws->aws;provider.aws.west->aws_west;- other provider local names and aliases use the same lower-case, identifier-safe normalization.
Aliased providers remain distinct. Provider requirements, source addresses, version constraints, resource/data-source provider references, and module provider mappings remain review facts. The adapter must not resolve provider credentials, endpoints, regions, accounts, tenants, or auth flows from the environment.
Symbolic Values
The adapter preserves variables, locals, outputs, references, expressions,
count, for_each, depends_on, lifecycle facts, module inputs, and module
provider mappings as symbolic review text. Literal values may be carried only
when they are not sensitive and do not look secret-like.
The adapter must not invent runtime values. Unknowns, expressions, unresolved
references, dynamic counts, and dynamic for_each values remain symbolic and
may produce TODO diagnostics when they block confident OpenAPI request mapping.
OpenAPI Operation Matching
For each selected data source or managed-resource action, the adapter builds operation-selection hints from prompt-safe facts:
- provider local name or normalized provider binding;
- action purpose:
read,list,create,update, ordelete; - Terraform address, type, name, and attribute names;
- OpenAPI ID.
Candidates are sorted deterministically by OpenAPI ID and
SortedOperationSummaries, then selected with SelectOperationByHints. A
single confident match becomes a draft operation reference shaped as
{openapi_id, operation_id}. No match produces an unresolved operation TODO. An
ambiguous match produces an ambiguity TODO listing deterministic namespaced
candidate operation references.
TODO operation IDs are deterministic and derived from address, purpose, and action. They must not include timestamps, random suffixes, process IDs, or machine-specific paths. A conventional shape is:
todo.<normalized-address>.<purpose>.<action>
For data sources, the action segment should be read or list. For
--action replace, the adapter emits separate delete and create TODO IDs using
the same target address.
Redaction And Sensitive Inputs
tfconfig sensitive values and sensitive candidates become sensitive OpenUdon
inputs or review variables. Generated artifacts must never include raw
secret-like literal values.
Redaction behavior:
- preserve the source address, attribute path, and redaction reason;
- replace the literal with a symbolic sensitive variable name in generated review scaffolding;
- emit a review TODO when a reviewer must confirm the redaction or binding;
- avoid writing secret-like literal defaults, examples, comments, or Markdown prose.
Diagnostics And Strict Mode
Diagnostics are emitted to JSON and Markdown. JSON records use stable fields:
| Field | Meaning |
|---|---|
code |
Stable machine-readable code. |
severity |
info, warning, or error. |
message |
Human-readable diagnostic text. |
address |
Terraform resource, data-source, provider, module-call, or target address when applicable. |
module_address |
tfconfig module address when applicable. |
source_range |
Source path/range from tfconfig or OpenAPI input when available. |
todo_id |
Deterministic TODO identifier when the diagnostic creates reviewer work. |
strict_failure |
Boolean indicating whether --strict turns this diagnostic into command failure. |
Strict mode fails on:
- parser error diagnostics from
tfconfig; - missing, unreadable, malformed, or duplicate-ID OpenAPI documents;
- unmatched
--targetvalues; - selected managed resources without a valid required
--action; - unresolved operation TODOs;
- ambiguous operation matches;
- unreviewed sensitive redaction TODOs.
Non-strict mode still emits the same diagnostics and review TODOs but may exit successfully if artifact writing succeeds.
Diagnostics and TODOs are sorted by code, address, module_address, and
todo_id, with empty fields sorted before non-empty fields. Markdown mirrors
the JSON order.
Determinism
Generated output must be stable for review:
- sort OpenAPI documents by ID;
- sort targets by exact address;
- preserve modules, resources, data sources, provider configs, variables,
locals, outputs, diagnostics, and source files in
tfconfigcanonical order; - sort provider bindings by normalized binding name;
- sort operation candidates by OpenAPI ID then operation ID;
- sort diagnostics and TODOs by stable fields;
- use relative, normalized paths under the output directory where possible;
- avoid timestamps, random values, absolute temp paths, hostnames, usernames, environment-derived credentials, and network fetches.
The converter must be static and local. It must not run provider plugins,
tofu init, module downloads, backend initialization, state loading, refresh,
plan, apply, or OpenAPI operations.
Draft Output
M8 should write the draft layout below by default under ./.openudon/convert:
.openudon/convert/
project.md
workflows/
intent.hcl
expected/
diagnostics.json
diagnostics.md
review.md
Artifact roles:
project.mdsummarizes selected modules, resources, data sources, provider bindings, OpenAPI IDs, action policy, and unresolved review work.workflows/intent.hclcontains symbolic draft intent suitable for later OpenUdon package generation, clearly marked as unapproved review scaffolding.expected/diagnostics.jsoncontains the stable diagnostic records.expected/diagnostics.mdcontains reviewer-friendly diagnostics in the same deterministic order.expected/review.mdexplains operation mappings, redactions, provider bindings, symbolic values, and TODOs.
M8 may copy or reference OpenAPI inputs under openapi/ only if the
implementation needs package-local inputs. The initial contract does not require
embedding OpenAPI documents into the draft output.
The target output converges on normal OpenUdon package artifacts:
workflows/workflow.hcl
workflows/workflow.uws.yaml
expected/plan.json
expected/plan.md
expected/discovery.json
expected/review.md
expected/quality.json
expected/quality.md
expected/symphony-handoff.json
Generated artifacts remain unapproved until normal OpenUdon review, quality, approval, digest, and trusted-runner handoff checks pass.
M8 Implementation Notes
Implement M8 in ../openudon as a thin CLI route plus reusable
internal/tfconvert package. Keep package tests focused on deterministic
conversion from in-memory tfconfig facts and apitools inventories, then add
CLI tests for flag parsing, target errors, strict failures, and output paths.
The implementation should keep OpenUdon free of direct OpenTofu internals and
should rely on openudon check-apitools-boundary to prevent regressions to old
apitools lifecycle APIs.