Skip to content

Partner teams: devzero_team, and a team_id on devzero_cluster that never moves a cluster - #31

Merged
lbagic merged 17 commits into
mainfrom
lb/partner-teams-provider
Nov 4, 2026
Merged

lbagic merged 17 commits into
mainfrom
lb/partner-teams-provider

Conversation

@lbagic

@lbagic lbagic commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

A partner team can start a DevZero team for each of its customers with devzero_team, and register the customer's clusters there with devzero_cluster.team_id, all from Terraform.

Check that an upgrade leaves existing clusters alone: a state without team_id plans no diff, and no change of team ever replaces or moves a cluster. Risk: medium, a published resource schema.

Why

Partners install DevZero into their customers' clusters and want each customer in a team of its own, paid by the customer, set up end to end from Terraform with no clicks. The DevZero API now lets a partner team start such a team (TeamService/CreateTeam with team_id), and lets the partner's service account act in it. This release is held until that API is in production.

Read first

  1. internal/provider/cluster_resource.go: team_id is optional, computed and kept from state; every call goes to the cluster's team; ModifyPlan refuses any change of team on an existing cluster; Read fails, rather than dropping the cluster, when one outside the provider's team is gone or one with no stored team_id is not found; import takes <cluster_id> (the provider's team) or <team_id>/<cluster_id>.
  2. internal/provider/team_resource.go: devzero_team starts the team once; its starting values are create-only, Read never re-creates a lost team, Delete is the token owner's leave.
  3. docs/resources/team.md, docs/resources/cluster.md and examples/: drive it with a service account's token; what a destroy deletes; how to offboard a customer with removed blocks.
  4. internal/provider/cluster_team_test.go, team_resource_test.go: plan rows run through the framework's provider server, as under Terraform.

The rest: the test fake serves TeamService and scopes clusters by team; internal/proto and internal/gen copy DevZero's team.proto and user.proto with their generated code.

Evidence

On 75c384c: go build, go vet and go test -count=1 ./internal/provider/... pass; make generate leaves no diff; golangci-lint v2.12.2: 0 issues. Each new test fails under the bug it names (the token reset in the wrong team, the old Read's drop, a missed plain-id import's message).

Left out

  • Not run: acceptance tests against a live DevZero (TF_ACC); the partner API is not in production yet.
  • The cluster module's team_id input ships separately, after this release, so the example uses devzero_cluster directly.
  • Under -refresh=false on a state from the release before team_id, a renamed cluster can report an inconsistent result after apply, and a destroy reports done while the cluster stays registered in its old team. Refresh, the default, avoids both.
Review and detail
  • Upgrade: a state written before team_id decodes it as null; the first refresh fills it with the provider's team, and an unset team_id keeps its state value when the provider's team_id later changes.
  • A devzero_team renamed in configuration without a moved block is refused at plan when a devzero_cluster takes its team_id from it, before anything is started or left: otherwise the apply would start a second team, send its invites again, and leave the customer's real team. A standalone devzero_team is not refused, and its docs say to add a moved block.
  • A cluster whose state has no team_id yet, not found by its first refresh, fails and stays in state, since the provider's team may have changed in the same run; the error names the ways out: terraform state rm, a removed block with lifecycle { destroy = false } (Terraform 1.9.3 or later), terraform apply -refresh-only -target=<address> with the provider's old team_id, or a re-import as <team_id>/<cluster_id>.
  • team_id = "" is refused at validate: it would register the cluster in the provider's team and taint it on every apply.
  • With a person's token, a destroy of devzero_team is that person's leave, and the last member's leave deletes the team; DevZero refuses it in several cases (the docs name the likely ones).
  • The first apply after a <team_id>/<cluster_id> import issues a new cluster token, which the cluster's agents then need (the docs say so).
  • No CHANGELOG entry: the file holds only the scaffold's 0.1.0 (Unreleased).
  • Rollback: a revert before release changes nothing for users. After release, a user who added team_id or devzero_team cannot go back to an older provider version without removing them from configuration.

lbagic added 12 commits November 2, 2026 11:14
A team started from the provider's team: Create calls CreateTeam with the provider's team_id and turns invites_not_sent into a warning; Read fails rather than dropping a team it cannot read; Delete leaves the team; import by id. The values the team is started with are refused when changed, except after an import.
team_id is optional and computed, defaults to the provider's team, and a planned change of a known value is refused. Read fills it in for a state written without one, and fails instead of dropping a cluster that is not in the provider's own team. Import also takes <team_id>/<cluster_id>.
The devzero_team example is a partner's onboarding configuration: the provider as the partner's team with a service account's token, the customer's team, and the customer's cluster registered in it.
… the provider's own team alone, and the docs say what a destroy deletes
…n refuses a known new email beside an unknown one
Copilot AI balanced review requested due to automatic review settings November 3, 2026 17:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Legacy-team lookup failures can remove existing clusters from state and trigger duplicate registration, while cross-team token-reset coverage and standalone team-rename guidance remain incomplete.

3 open issues
2 files not reviewed
  • internal/gen/api/v1/apiv1connect/team.connect.go: Generated file
  • internal/gen/api/v1/user.pb.go: Generated file
What changed in this PR

Adds partner-customer onboarding to the Terraform provider through devzero_team and per-cluster team selection, with safeguards intended to preserve existing clusters.

Changes:

  • Introduces team creation, import, refresh, and leave behavior.
  • Adds cluster team_id, team-scoped API calls, and cross-team imports.
  • Adds API bindings, lifecycle tests, and onboarding and offboarding documentation.
File Description
Makefile Adds team and user API files to copy lists.
internal/​provider/​team_resource.go Implements the customer-team resource lifecycle.
internal/​provider/​team_resource_test.go Tests team lifecycle and create-only attributes.
internal/​provider/​schema_model_consistency_test.go Includes the team schema/model checks.
internal/​provider/​provider.go Registers the team resource and API client.
internal/​provider/​fake_server_test.go Adds team-service support and team-scoped test state.
internal/​provider/​fake_server_lifecycle_extra_test.go Enforces cluster team scoping in the fake backend.
internal/​provider/​cluster_team_test.go Tests cluster team planning, routing, and imports.
internal/​provider/​cluster_resource.go Adds team selection, change guards, and scoped imports.
internal/​proto/​api/​v1/​user.proto Adds user message definitions required by the team API.
internal/​proto/​api/​v1/​team.proto Adds team-service API definitions.
internal/​gen/​api/​v1/​user.pb.go Adds generated user protobuf bindings.
internal/​gen/​api/​v1/​apiv1connect/​team.connect.go Adds generated team-service clients and handlers.
examples/​resources/​devzero_team/​resource.tf Demonstrates customer-team and cluster onboarding.
examples/​resources/​devzero_team/​import.sh Shows team import syntax.
examples/​resources/​devzero_cluster/​import.sh Adds cross-team import syntax and token warnings.
docs/​resources/​team.md Documents team configuration and offboarding behavior.
docs/​resources/​cluster.md Documents cluster team selection and imports.

Review effort: Balanced

Comment thread internal/provider/cluster_resource.go Resolved
Comment thread internal/provider/cluster_resource.go Resolved
Comment thread internal/provider/team_resource.go Resolved
… to the imported team

The fake's ResetClusterToken now answers NotFound in a team the cluster is not in, as its other cluster RPCs do. The new test imports <team_id>/<cluster_id> and runs the token-issuing Update; it fails if that reset goes to the provider's team.
…d block

Without one, a standalone devzero_team starts a second team, sends its invites again and leaves the original; only a devzero_cluster whose team_id comes from the team turns the rename into a plan refusal.
…at its first refresh cannot find

A state without team_id takes the provider's team at its first refresh. When the cluster is not found there, Read now fails and keeps state instead of dropping it, since the provider's team_id may have changed in the same run and the next apply would register a second cluster. A gone cluster whose stored team is the provider's is still dropped.
A removed block offered for a cluster or team that cannot be read is one that sets lifecycle { destroy = false }, on Terraform 1.9.3 or later, since an older one, or one that destroys, refreshes the object into the same error. The old-state refusal names -target for its refresh-only remedy and the new token a re-import issues. A plain <cluster_id> import stores the provider's team, so one that misses gets Terraform's own error rather than the old-state refusal. A test row reaches the no-cluster answer.
Copilot AI balanced review requested due to automatic review settings November 3, 2026 22:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human validation is needed because the partner API is not yet in production and live acceptance tests have not verified cross-team authorization or the service-account LeaveTeam behavior underlying destroy semantics.

No open issues 🎉

3 resolved since last review
2 files not reviewed
  • internal/gen/api/v1/apiv1connect/team.connect.go: Generated file
  • internal/gen/api/v1/user.pb.go: Generated file

Review effort: Balanced

@lbagic
lbagic merged commit 0cceeb3 into main Nov 4, 2026
15 checks passed
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.

3 participants