Skip to content

Fix edge stack Read to support non-destructive brownfield import - #126

Merged
grulicht merged 1 commit into
mainfrom
fix/edge-stack-brownfield-import
Jun 11, 2026
Merged

Fix edge stack Read to support non-destructive brownfield import#126
grulicht merged 1 commit into
mainfrom
fix/edge-stack-brownfield-import

Conversation

@TheGnaneswar

Copy link
Copy Markdown
Collaborator

fix(edge_stack): make brownfield import non-destructive

Summary

terraform import of an existing edge stack used to leave state nearly empty, so the very next terraform plan proposed destroying and recreating the stack — even when the config matched reality exactly. This made brownfield adoption of the provider unusable for git-backed edge stacks.

This PR fixes resourceEdgeStackRead to populate full state on import, and corrects one over-broad ForceNew flag so legitimate ref changes update in place instead of forcing replacement.

The problem

After terraform import portainer_edge_stack.x 1:

  • terraform show wrote only 4 fields to state — no git config, no edge groups, no deployment metadata.
  • terraform plan then showed -/+ resource ... must be replaced (1 to add, 1 to destroy), because the config looked like it was introducing attributes for the first time — and three of them (repository_url, repository_reference_name, file_path_in_repository) were ForceNew.
  • Applying that plan would DELETE /edge_stacks/1, recreate with a new ID, force every edge agent to rebind, and lose deployment history + the webhook UUID.

Root cause

  1. Read didn't populate state. resourceEdgeStackRead decoded only 6 of the ~20 fields GET /edge_stacks/{id} returns. Everything else stayed null, so plan treated the config as new attributes — and the ForceNew ones forced replacement.
  2. ForceNew over-applied. repository_reference_name was ForceNew even though Portainer's PUT /edge_stacks/{id}/git endpoint updates the ref in place via refName.

What changed

internal/resource_edge_stack.go:

  • Expanded the GET /edge_stacks/{id} response struct and added d.Set(...) for all returned fields: name, deployment_type, edge_groups, registries, use_manifest_namespaces, pre_pull_image, retry_deploy, always_clone, environment, relative_path, the git config block, and the AutoUpdate-derived fields.
  • Made GitConfig.Authentication a pointer so Read can distinguish "no auth configured" (nil) from a zero-valued credential ID, and set git_repository_authentication accordingly (the old value-struct could never represent the "no auth" case and wrote a phantom repository_git_credential_id = 0).
  • Added AutoUpdate.ForceUpdate to the decoded struct so force_update is populated instead of showing + false on every plan.
  • Set explicit defaults in Read for dryrun and the AutoUpdate-derived fields (pull_image, force_update, stack_webhook, update_interval) so a stack without GitOps doesn't produce spurious diffs.
  • Dropped ForceNew from repository_reference_name only — it's the one git field PUT /git mutates in place. repository_url, file_path_in_repository, repository_username, repository_password, relative_path, and stack_file_path keep ForceNew (the git endpoint silently ignores those in the payload — see the comment added to resourceEdgeStackUpdate).

docs/resources/edge_stack.md:

  • Documented the post-import verification flow and which fields update in place vs. force recreation.

Behavior contrast

Scenario Before After
Fields populated by Read after import 4 19
terraform plan on matching config -/+ replace (1 add, 1 destroy) No changes
Change repository_reference_name -/+ replace (false positive) ~ update in-place, 0 destroy
Change repository_url / file_path_in_repository -/+ replace -/+ replace (correct — API can't repoint source)
Stack ID / agent bindings preserved on ref change No Yes
dryrun/force_update/pull_image/stack_webhook plan noise + false every plan Stable, no diff

How it was verified

  • go build ./... ✅, go vet ./internal/
  • Unit tests: go test ./internal -run EdgeStack ✅ (incl. TestEdgeStackRead_HappyPath)
  • Live, against a real Portainer instance:
    • Imported an existing production edge stack (ID 1) → terraform show now writes 19 populated fields → terraform plan reports No changes. Your infrastructure matches the configuration.
    • Changed repository_reference_name → plan shows ~ update in-place, Plan: 0 to add, 1 to change, 0 to destroy (stack ID preserved).
  • CI pr-e2e-test exercises the edge_repository full create→update→delete cycle, covering the changed Read + ForceNew paths.

Known follow-ups (out of scope here)

  • repository_url / file_path_in_repository remain ForceNew — Portainer's EE PUT /git endpoint does not expose source-URL or in-repo file-path mutation.
  • repository_username, repository_password, relative_path remain ForceNew pending a live test against a stack with auth / relative paths configured.
  • stack_file_path remains ForceNew — Update has no multipart re-upload branch yet.

…non-destructive

resourceEdgeStackRead only decoded 6 of the ~20 fields the Portainer API
returns, leaving git config, edge groups, and deployment metadata null in
state. After `terraform import`, plan saw the config as introducing those
attributes for the first time, and since three were ForceNew the only legal
plan was destroy + recreate -- making brownfield adoption unusable.

Changes:
- Expand the GET /edge_stacks/{id} response struct and add d.Set calls for
  all returned fields (name, deployment_type, edge_groups, registries, git
  config, AutoUpdate-derived fields, webhook, etc.).
- Make GitConfig.Authentication a pointer so Read can distinguish "no auth
  configured" (nil) from a zero-valued credential id, and set
  git_repository_authentication accordingly.
- Add AutoUpdate.ForceUpdate to the decoded struct so force_update is
  populated instead of showing as `+ false` on every plan.
- Set explicit defaults in Read for dryrun and AutoUpdate-derived fields to
  stop spurious diffs when GitOps is not configured.
- Drop ForceNew from repository_reference_name only -- PUT /edge_stacks/{id}/git
  honors refName in place. URL, file path, username, password, relative_path,
  and stack_file_path keep ForceNew (the git endpoint silently ignores them).
- Document the API contract in resourceEdgeStackUpdate and the field
  update-in-place vs ForceNew behavior in docs/resources/edge_stack.md.

Verified live: import of an existing stack now yields `No changes`, and a
repository_reference_name change plans as `~ update in-place` (0 to destroy).
@TheGnaneswar
TheGnaneswar requested a review from grulicht June 3, 2026 11:07
@TheGnaneswar
TheGnaneswar marked this pull request as ready for review June 3, 2026 11:09
@TheGnaneswar TheGnaneswar self-assigned this Jun 5, 2026
@grulicht
grulicht merged commit 7d6a8c7 into main Jun 11, 2026
6 of 9 checks passed
@grulicht
grulicht deleted the fix/edge-stack-brownfield-import branch June 11, 2026 05:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants