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
4 changes: 4 additions & 0 deletions docs/opentofu-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ ct ids sync --env prod --tofu-state terraform.tfstate --dry-run
ct ids list --env prod # what ct would resolve through
```

Role definitions (`churchtools_group_role_definition`, tier 3) are read too, into
the `group-role` section, so `{ roleDef: "…" }` references keep resolving once the
roles leave ct's state. They are read-only here: `ct export tf` does not write them.

`sync` reports every id it adds, changes or drops, and **refuses to replace a
populated map with an empty one** — the likeliest cause of an empty read is the
wrong workspace, and overwriting 50 working ids with nothing would break every
Expand Down
10 changes: 7 additions & 3 deletions src/resolve/tfstate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ import type { IdMapEntry } from "./idMap.js";

/** Terraform resource type → ct resource type, derived from the exporter's own table so the two
* directions cannot drift. */
const CT_TYPE_BY_HCL_TYPE: ReadonlyMap<string, string> = new Map(
EXPORTABLE_TYPES.map((ctType) => [hclType(ctType), ctType] as const),
);
const CT_TYPE_BY_HCL_TYPE: ReadonlyMap<string, string> = new Map([
...EXPORTABLE_TYPES.map((ctType) => [hclType(ctType), ctType] as const),
// Role definitions (`/group/roles`) are hand-written in HCL once tofu owns them, so ct never
// EXPORTS them (they are not in EXPORTABLE_TYPES); but a `role-def` reference still has to resolve
// to their ids after the entries leave ct's state. Read-only mapping: state → id map.
["churchtools_group_role_definition", "group-role"] as const,
]);

export interface TfStateReadResult {
entries: IdMapEntry[];
Expand Down
36 changes: 36 additions & 0 deletions tests/tofu-id-map.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,17 @@ describe("Resolver", () => {
expect(await resolver.resolve(ref.campus("mainz"), "site")).toBe(0);
});

it("resolves a role definition once it left ct's state (tier 3)", async () => {
// `roleDef` reads type "group-role"; after `ct state rm group-role <key>` only the map knows it.
const map = parseIdMap(
renderIdMap(HOST, [{ type: "group-role", key: "team_member", id: 7 }], "ct ids sync"),
".ct/ids.json",
HOST,
);
const resolver = new Resolver({ client: client as never, state, desired: [], host: HOST, idMap: map });
expect(await resolver.resolve(ref.roleDef("team_member"), "site")).toBe(7);
});

it("still prefers ct's own state over the map", async () => {
// ct never stops trusting what it owns: a resource still in ct's state is ct's, and a stale map
// entry must not overrule it.
Expand Down Expand Up @@ -197,6 +208,31 @@ describe("readTfState", () => {
expect(serial).toBe(12);
});

it("maps churchtools_group_role_definition to the group-role type", () => {
const { entries, unmapped } = readTfState({
resources: [
{
mode: "managed",
type: "churchtools_group_role_definition",
name: "team_member",
instances: [{ attributes: { id: "7" } }],
},
// for_each blocks stay unmapped, as for every other type.
{
mode: "managed",
type: "churchtools_group_role_definition",
name: "bulk",
instances: [
{ index_key: "a", attributes: { id: "1" } },
{ index_key: "b", attributes: { id: "2" } },
],
},
],
});
expect(entries).toEqual([{ type: "group-role", key: "team_member", id: 7 }]);
expect(unmapped).toEqual([]);
});

it("takes the label as the key when no previous map recorded a relabelling", () => {
const { entries } = readTfState(tfstate);
expect(entries.map((e) => e.key)).toEqual(["status_unbekannt", "g_3_groupactive"]);
Expand Down
Loading