Repository navigation
Add the join key and host resources - #7
johnmaguire wants to merge 1 commit into
Conversation
definednet_join_key manages a reusable enrollment secret scoped to a network, role and tags. The shape is immutable, so changing it replaces the key; name and expires_at update in place. The token is returned only on create and is stored sensitive in state. Hosts enrolled with a key stay outside Terraform. definednet_host manages a host record for lighthouses, relays and other hosts whose identity must exist before they boot. ipv4_address can be set at create or added in place to a host without one; ip_addresses mirrors the API's list. Enrollment codes are minted outside Terraform. A plan-time check on both the join key and the endpoint OIDC provider rejects an empty auto_assign_cidrs on a certificate version 1 network, where the API always assigns IPv4. For a join key the check matters because a replace deletes the old key before the API could reject the new one. The host is created through POST /v2/host-and-enrollment-code so its single-use enrollment code lands in state (sensitive) and can feed the machine's user data. A change to the enrollment_code_triggers map mints a new code; the triggers of an existing host must be known at plan time, because Terraform re-plans during apply and a value resolving equal to the stored map would keep a consumed code after the dependent machine was destroyed. Update sends the PUT only when a field it owns changed, since the API writes an audit entry for every PUT, and commits the add-IP and PUT results with the previous code and triggers, so a failed mint on a blocked host leaves the trigger change pending. GET never returns the code, so Read carries it forward and an imported host has none until its triggers change. The lifetime is stored only as configured; omitted, the API applies its one-day default. The combined route needs the hosts:enroll scope as well.
nbrownus
left a comment
There was a problem hiding this comment.
Went through this with repro tests against the fake API. Three behavior problems (1-3 inline, all reproduced), plus a few small cleanups.
- The cert v1 plan check breaks existing OIDC provider configs, including destroy.
- Any changed plan with
auto_assign_cidrs = []needsnetworks:read, even when the change has nothing to do with CIDRs. - Unknown
enrollment_code_triggersare rejected on a host that is being replaced anyway.
The rest are nits, take them or leave them.
| } | ||
| // No apply follows an unchanged plan, so a failed read must not block it | ||
| // (terraform destroy runs one before the destroy plan). | ||
| if !req.State.Raw.IsNull() && req.Plan.Raw.Equal(req.State.Raw) { |
There was a problem hiding this comment.
This early return doesn't save existing configs. On main the API (and the fake) accepted auto_assign_cidrs = [] on a cert v1 network and stored IPv4 anyway, and the docs called that a perpetual diff. Config [] vs state [v4] never matches, so this check now fails every plan for those users, and terraform destroy too since it runs a normal plan first.
Repro: create the provider with [] on a v2 network, flip the fake network to CertVersion = 1 and set the stored AutoAssignCIDRs to the v4 prefix, then plan and destroy. Both fail with "Empty auto_assign_cidrs on a certificate version 1 network".
Could we only error when the resource is new or auto_assign_cidrs / network_id actually changed? Either way this needs a breaking-change note.
| return | ||
| } | ||
| id := networkID.ValueString() | ||
| n, err := c.GetNetwork(ctx, id) |
There was a problem hiding this comment.
I see the join key test asserts this on purpose, but it's broader than the check needs. Renaming a provider (or rotating its secret) with auto_assign_cidrs = [] now reads the network, so a key without networks:read, a transient API error, or a network deleted out of band fails an update that never touches CIDRs. Reproduced with a rename after setting missingScopes["networks:read"].
Same suggestion as above: only do the read on create, or when auto_assign_cidrs / network_id changed. For the join key that's create/replace only, since nothing else updates in place.
| } | ||
|
|
||
| func (enrollmentCodeUseStateUnlessReminted) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { | ||
| if req.State.Raw.IsNull() || !req.PlanValue.IsUnknown() { |
There was a problem hiding this comment.
This rejects unknown triggers even when the host is being replaced (say network_id changes), where Create always mints a fresh code and the consumed-code hazard can't happen. Repro: host with enrollment_code_triggers = { a = "1" }, then move it to a new definednet_network.m with enrollment_code_triggers = { a = definednet_network.m.id }. That fails with "Unknown enrollment_code_triggers". That's exactly the AMI-copy-plus-replace shape people will hit.
An attribute modifier can't easily see the pending replace, so this probably wants to move to resource-level ModifyPlan, after resp.RequiresReplace is known, or skip the error when any RequiresReplace attribute differs from state.
|
|
||
| // samePUTFields reports whether two PUT bodies describe the same host. Sets | ||
| // carry no order, and override values are JSON whose spelling may differ. | ||
| func samePUTFields(a, b client.HostUpdateRequest) bool { |
There was a problem hiding this comment.
Nit: comparing plan to state field by field with .Equal() would replace samePUTFields, optionalStringEqual and mapSlice. Sets are order-free and flattenConfigOverrides already keeps the configured spelling. The one thing you'd give up is skipping the PUT when someone re-spells override JSON in config, which seems fine to me.
| } | ||
|
|
||
| ipv4 := types.StringNull() | ||
| for _, a := range h.IPAddresses { |
There was a problem hiding this comment.
Nit: this is ipv4Only again (keeping the last match instead of all of them). if v4 := ipv4Only(h.IPAddresses); len(v4) > 0 keeps the family test in one place.
| n, err := r.client.GetNetwork(ctx, body.NetworkID) | ||
| if err != nil { | ||
| resp.Diagnostics.AddError("Unable to create host", | ||
| fmt.Sprintf("host %q: reading network %q to assign an IPv4 address: %s", body.Name, body.NetworkID, err)) |
There was a problem hiding this comment.
Nit: describeError(err) here like the CreateHostAndEnrollmentCode error below, so a missing networks:read gets the same hint.
definednet_join_key manages a reusable enrollment secret scoped to a network,
role and tags. The shape is immutable, so changing it replaces the key; name
and expires_at update in place. The token is returned only on create and is
stored sensitive in state. Hosts enrolled with a key stay outside Terraform.
definednet_host manages a host record for lighthouses, relays and other hosts
whose identity must exist before they boot. ipv4_address can be set at create
or added in place to a host without one; ip_addresses mirrors the API's list.
Enrollment codes are minted outside Terraform.
A plan-time check on both the join key and the endpoint OIDC provider rejects
an empty auto_assign_cidrs on a certificate version 1 network, where the API
always assigns IPv4. For a join key the check matters because a replace
deletes the old key before the API could reject the new one.
The host is created through POST /v2/host-and-enrollment-code so its
single-use enrollment code lands in state (sensitive) and can feed the
machine's user data. A change to the enrollment_code_triggers map mints a
new code; the triggers of an existing host must be known at plan time,
because Terraform re-plans during apply and a value resolving equal to the
stored map would keep a consumed code after the dependent machine was
destroyed. Update sends the PUT only when a field it owns changed, since the API
writes an audit entry for every PUT, and commits the add-IP and PUT results
with the previous code and triggers, so a failed mint on a blocked host
leaves the trigger change pending. GET never returns the code, so Read carries it forward and an
imported host has none until its triggers change. The lifetime is stored
only as configured; omitted, the API applies its one-day default. The
combined route needs the hosts:enroll scope as well.
Verification
go test ./...green (fake API).TF_ACC=1suite passed live against the test org, 26/26, including the host and join key tests: the combinedcreate route mints codes, a trigger change mints a fresh one, add-IP in place, out-of-band deletes, and join
keys (the JOIN_KEYS flag is on for the test org).
round on the enrollment code, and a light review of the PUT skip.
Notes for the reviewer
hosts:enrollin addition to the host scopes, becausePOST /v2/host-and-enrollment-coderequires both.
enrollment_code_triggersmust be fully known on an existing host; the planmodifier rejects unknown values with an error that says how to fix it.
expires_atis a UTC-only...Zstring compared as text; the API always returns UTC whole seconds.