Fix edge stack Read to support non-destructive brownfield import - #126
Merged
Conversation
…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
marked this pull request as ready for review
June 3, 2026 11:09
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(edge_stack): make brownfield import non-destructive
Summary
terraform importof an existing edge stack used to leave state nearly empty, so the very nextterraform planproposed 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
resourceEdgeStackReadto populate full state on import, and corrects one over-broadForceNewflag so legitimate ref changes update in place instead of forcing replacement.The problem
After
terraform import portainer_edge_stack.x 1:terraform showwrote only 4 fields to state — no git config, no edge groups, no deployment metadata.terraform planthen 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) wereForceNew.DELETE /edge_stacks/1, recreate with a new ID, force every edge agent to rebind, and lose deployment history + the webhook UUID.Root cause
Readdidn't populate state.resourceEdgeStackReaddecoded only 6 of the ~20 fieldsGET /edge_stacks/{id}returns. Everything else stayed null, so plan treated the config as new attributes — and theForceNewones forced replacement.ForceNewover-applied.repository_reference_namewasForceNeweven though Portainer'sPUT /edge_stacks/{id}/gitendpoint updates the ref in place viarefName.What changed
internal/resource_edge_stack.go:GET /edge_stacks/{id}response struct and addedd.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 theAutoUpdate-derived fields.GitConfig.Authenticationa pointer so Read can distinguish "no auth configured" (nil) from a zero-valued credential ID, and setgit_repository_authenticationaccordingly (the old value-struct could never represent the "no auth" case and wrote a phantomrepository_git_credential_id = 0).AutoUpdate.ForceUpdateto the decoded struct soforce_updateis populated instead of showing+ falseon every plan.dryrunand theAutoUpdate-derived fields (pull_image,force_update,stack_webhook,update_interval) so a stack without GitOps doesn't produce spurious diffs.ForceNewfromrepository_reference_nameonly — it's the one git fieldPUT /gitmutates in place.repository_url,file_path_in_repository,repository_username,repository_password,relative_path, andstack_file_pathkeepForceNew(the git endpoint silently ignores those in the payload — see the comment added toresourceEdgeStackUpdate).docs/resources/edge_stack.md:Behavior contrast
terraform planon matching config-/+ replace(1 add, 1 destroy)No changesrepository_reference_name-/+ replace(false positive)~ update in-place, 0 destroyrepository_url/file_path_in_repository-/+ replace-/+ replace(correct — API can't repoint source)dryrun/force_update/pull_image/stack_webhookplan noise+ falseevery planHow it was verified
go build ./...✅,go vet ./internal/✅go test ./internal -run EdgeStack✅ (incl.TestEdgeStackRead_HappyPath)terraform shownow writes 19 populated fields →terraform planreportsNo changes. Your infrastructure matches the configuration.repository_reference_name→ plan shows~ update in-place,Plan: 0 to add, 1 to change, 0 to destroy(stack ID preserved).pr-e2e-testexercises theedge_repositoryfull create→update→delete cycle, covering the changed Read +ForceNewpaths.Known follow-ups (out of scope here)
repository_url/file_path_in_repositoryremainForceNew— Portainer's EEPUT /gitendpoint does not expose source-URL or in-repo file-path mutation.repository_username,repository_password,relative_pathremainForceNewpending a live test against a stack with auth / relative paths configured.stack_file_pathremainsForceNew— Update has no multipart re-upload branch yet.