feat: bootstrap the meshStack CLI and move the API client in - #1
feat: bootstrap the meshStack CLI and move the API client in#1grubmeshi wants to merge 204 commits into
Conversation
meshStack enforces the Accept header soon, so we have to make sure to always provide it
includes adaptations from PR remarks
The meshTenant API deprecated the list-form spec.quotas in favour of the spec.requested_quotas map, which v0.24.3 added together with the computed status.applied_quotas. This drops the deprecated field from meshstack_tenant, its data source and meshstack_tenants, so the provider stops sending and modelling it. Only the unsuffixed, ref-based resource is touched. The deprecated meshstack_tenant_v4 keeps its own spec.quotas: it is removed wholesale once the meshTenant API goes GA. Existing state migrates automatically (schema version 1 -> 2): a quota recorded under spec.quotas is translated into spec.requested_quotas rather than dropped, so a configuration that restates the same quotas in the map form plans no change. That matters because a quota change on an existing tenant is rejected -- the meshTenant API cannot update one. The `moved` mover from meshstack_tenant_v4 translates the same way. Because this only stops using a field the API still accepts, it needs no newer meshStack version and the version floor stays at 2026.30.0. Issue: CU-86c0j0r7q
Send the GA `v4` meshTenant media type instead of `v4-preview` from meshstack_tenant and meshstack_tenants, and drop the preview disclaimer from their documentation. This requires a meshStack backend that has promoted meshTenant v4 to GA; a backend that only serves the preview media type answers with HTTP 415. With the unsuffixed resource on the GA API, the deprecated meshstack_tenant_v4 resource and data source go away, together with their client (client.MeshTenantV4), models, mocks, builder, examples and docs, and their registration in the provider. The meshstack_tenant MoveState/moveFromV4 migration path goes with them, because the type it migrates from no longer exists -- so apply that `moved` block on v0.24.x before you upgrade. client/tenant_v4_test.go becomes client/tenant_v4_deletion_test.go, since the deletion helpers are all it still covers. The client package keeps the v4 in its file names, because it is the API version it talks to. The version floor moves to 2026.34.0, the first release that can still carry the backend flip: v2026.33.0 was tagged 2026-08-12 while the backend PR was open. docs/index.md is regenerated from the __MIN_MESHSTACK_VERSION__ placeholder in templates/index.md.tmpl rather than edited by hand. Issue: CU-86c0j0r7q
451238f to
d39e4c1
Compare
Stand up the repository so the meshStack API client has somewhere to move to: a Taskfile, a Go-only CI workflow, a Nix dev shell and a cobra root command that prints help and nothing else yet. The dependency policy is the part worth reading. This repository is allowed one external dependency, cobra, and only cmd/ may use it; everything else stays on the standard library, with testify permitted in tests. The reason is that the Terraform provider will import client/ and pkg/login, so anything added here lands in the provider's dependency tree and in the public checksum database. depguard in .golangci.yml enforces that per directory. Two things about those depguard patterns. A rule that matches no file is a silent no-op, so each rule was verified by compiling a file that violates it. That is how the pattern bug surfaced: '**/client/**/*.go' matches files in subdirectories of client/ only, never files directly inside it, which would have left most of the client unguarded. Both patterns are therefore listed. The binary is meshstack while the module is meshstack-cli, and the layout is what reconciles them: the main package sits in cmd/meshstack, so 'go build' and 'go install' name the binary after that directory. Neither needs -o, and there is no main.go at the repository root, which would have named the binary after the module instead. cmd/meshstack holds main() and the root command together and is the one directory under cmd/ that is not a subcommand. The root command sets RunE even though it has no work to do, because cobra's help template skips the usage block entirely while a command is neither runnable nor a parent of subcommands — without it, 'meshstack --help' printed one line and no flags at all. Args is NoArgs so an unknown argument fails instead of silently printing help. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
d39e4c1 to
f329c91
Compare
The opt-out was evaluated inside the version-mismatch branch, so it was only reachable once GET /mesh/info had already succeeded. Setting the flag therefore never skipped the request — it only suppressed a version mismatch. /mesh/info is a GET on the retrying client, so an unavailable meshStack made every provider configure block for the client's full retry budget (~4 minutes) and then fail, with no way to opt out. Move the check to the top of checkMeshVersion so the flag short-circuits before the request is built. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A building block plan converted spec out of tfsdk.Plan, which fails whenever an attribute is wired to a resource the same plan creates or replaces. ModifyPlan now walks the planned spec for unknowns and schedules a run instead, matching what it already did for an unknown definition version ref. meshstack_tenant_v4 also lost spec.platform_identifier and spec.landing_zone_identifier against a meshStack that serves meshTenant v4 in its ref shape. Both force replacement, so a refresh planned the recreation of a live tenant. They are recovered from spec.landingZoneRef.name and from status.tenantName, without relying on the dropped flat identifiers. Together these unblock destroying a building block composition created before the backend moved meshTenant v4 to refs.
Resolutions: - CHANGELOG.md: keep both sections, v0.25.0 above the v0.24.5 patch. - client/tenant_v4.go and its tests: take main's removal. The meshTenant v4 ref read fix only applies to meshstack_tenant_v4, which v0.25.0 drops; it stays on the v0.24.5 tag. The unknown-spec fix in ModifyPlan carries forward.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e_flags Replaces the is_four_eyes_enabled bool with a more general enabled_feature_flags set-of-strings attribute (currently only four_eyes_role_approval), per PR review.
Brings the meshStack API client in with its history — 197 commits reaching back
to 'refactor: move client to separate package' — rather than as a flat copy, so
authorship and per-change granularity survive the move.
Split from terraform-provider-meshstack's main with
'git subtree split --prefix=client', then added with 'git subtree add', which
means future changes can be carried between the two repositories with
'git subtree pull' and 'git subtree push'.
How to read that history, because the paths shift at this commit. The split
history carries the files at the repository root (client.go, not
client/client.go) and this merge re-roots them under client/, so a path-limited
log from the new path stops here:
git log -- client/client.go # only this commit
git log -- client/client.go client.go # the full 58
git blame client/client.go # traverses the merge correctly
'git log --follow' does not help: it resolves renames within one commit's
parents, not across a subtree re-rooting.
The files arrive verbatim, so they still import
github.com/meshcloud/terraform-provider-meshstack/client at this commit and do
not compile in this module yet. The next commit rewrites those import paths.
Adapting them here would have meant rewriting the content of all 197 commits,
which is exactly what not doing this as a copy was meant to avoid.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rewrites the client's own import path in the 38 files that reference it, from github.com/meshcloud/terraform-provider-meshstack/client to github.com/meshcloud/meshstack-cli/client. Nothing else changes: the package keeps the 'client' prefix it had in the provider, so this is the only edit the move needs and future 'git subtree pull' carries changes across with a conflict only where a file genuinely diverged. This is the commit that makes the subtree compile in this module. It is separate from the subtree merge on purpose — folding it in would have meant rewriting the content of every imported commit. go.sum picks up testify, which the client's tests require. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pkg/login is the entry point both the CLI and the Terraform provider use to turn credentials into a client.Authorization. It resolves credentials and nothing more. The login exchange stays in client/internal/auth.go, where it already caches the access token and refreshes it before expiry; Go's internal rule keeps that code inside client/, and it is reached through client.NewApiKeyAuthorization. Writing a second exchange here would have produced a static token that starts returning 401 once it expires. The four MESHSTACK_ environment variable names are exported consts rather than private ones, because the provider's diagnostics quote the variable names in their message text; keeping them private would mean the same strings living in both repositories. Credentials.Merge exists for the provider: it merges provider block attributes over FromEnv so an explicitly configured attribute outranks the environment. One gap worth recording. Authorization.Header takes a client/internal.HttpClient, so no package outside client/ can call it, and nothing can read the cached token back out either. That does not matter yet, but caching a token in ~/.config/meshstack-cli will need a new exported seam. pkg/login is the only place that constructs an Authorization, which is where that seam belongs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the release path now, while the repository is still small, rather than discovering its rough edges at the first tag. Pushing a v* tag runs goreleaser, which publishes archives and checksums for linux, darwin and windows on amd64 and arm64, and then builds the container image for the same tag. The image build is a separate job so a failing image does not take the archives down with it, and it is a reusable workflow so a push to main can refresh :main through the same code path. Without that, no usable image would exist until the first release. Naming follows what the repository publishes rather than what it builds: the archives, the checksum file and the image are all meshstack-cli, while the binary inside them is meshstack. goreleaser's project_name carries the former and builds[].binary the latter, and the image entrypoint is the meshstack binary, so 'docker run ghcr.io/meshcloud/meshstack-cli buildingblock list' reads like the local invocation. The version reaches the binary as an ldflag on main.Version in cmd/meshstack, which the goreleaser config and the Dockerfile have to keep in agreement. A build without it reports 'dev'. Images go to GHCR only. Pull requests build the image without pushing it, so a broken Dockerfile fails review instead of main. The Dockerfile cross-compiles from the build platform using buildx's TARGETOS/TARGETARCH rather than emulating the target, and ships the binary on distroless static, which comes to 4.6 MB. Tags are computed in a shell step instead of with docker/metadata-action, to keep the set of SHA-pinned actions small. The action SHAs come from meshcloud/building-block-runner, whose image workflow this follows. Verified locally: 'goreleaser check' passes, 'goreleaser release --snapshot' produces meshstack-cli_*.tar.gz archives containing a meshstack binary that reports the injected version, and the image builds and runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The client came in with 'git subtree add' rather than as a copy, which changes two things a contributor needs to know and cannot guess from the tree. Changes travel with 'git subtree pull' and 'git subtree push' against the Terraform provider. A pull conflicts only where a file genuinely diverged, since the one local edit the move needed was rewriting the client's own import path. Reading the pre-import history takes both paths. The split history carries the files at the repository root and the import merge re-roots them under client/, so 'git log -- client/client.go' stops at the merge while 'git log -- client/client.go client.go' shows all of it. 'git blame' traverses the merge on its own, and 'git log --follow' does not help, because it resolves renames within a commit's parents rather than across a subtree re-rooting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
f329c91 to
163b976
Compare
|
Redone with
Commits
Still a clean fast-forward from ValidationYou asked me to prove that before and after differ only by client changes made in the provider meanwhile. Rather than eyeball a diff, I checked both ends by extracting all four trees and normalising the module path:
The observed before/after change set then matches the provider's own Whole-tree diff confirms nothing else moved: So my copy was stale by seven provider commits — the Two things worth knowingThe import merge does not compile on its own. History browsing needs both paths. The split history carries files at the repository root and the merge re-roots them under Re-verified after the rebuild: lint clean, all tests pass, |
2fe8df9 to
9c96db4
Compare
…hanged code
The lint job failed on this branch with a gofmt-class finding on
client/internal/retry_test.go:46 that no local run reproduces.
The cause is which Go built the linter, not which Go is on PATH. golangci-lint's
formatters use the go/format compiled into the binary, so 'version: latest' with
the default binary install downloaded a 2.13.0 built with Go 1.27, while go.mod
pins 1.26. Go 1.27 widens end-of-line comment alignment groups, so it wants that
table's comments aligned to a far wider column — and Go 1.26 then rejects the
result. The two are mutually exclusive: no formatting of that file satisfies both,
which rules out simply reformatting it.
So the linter is now built here from source with go.mod's Go
('install-mode: goinstall') and pinned to v2.13.0. Formatting is then decided by
the Go the code is written against, and a linter upgrade becomes a deliberate edit.
setup-go takes its version from go.mod for the same reason: it is what builds the
linter.
Verified by controlled comparison, because two earlier explanations were wrong.
Same config, same file, pristine checkout, empty cache: golangci-lint 2.13.0 built
with Go 1.26.5 reports a clean tree, the official 2.13.0 binary built with Go
1.27.0 reports the finding, and each rejects the other's preferred formatting.
Neither the patch-filtering mode nor the toolchain on PATH changes that.
Dropping only-new-issues is the second half. It exists so a pull request against a
large legacy codebase is not buried in pre-existing findings; this repository
starts clean and CI keeps it clean from the first commit, so filtering to changed
code cannot help and can only hide a real finding. It hid this one, and the
provider's identical job still hides it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
9c96db4 to
0b77443
Compare
|
CI is green. The lint failure after the subtree import took three attempts to diagnose, and the first two explanations were wrong — worth writing down because the cause is not obvious and the same trap sits in the provider's workflow. Symptom. What it was not. Not What it is. golangci-lint's formatters use the The two are mutually exclusive — Go 1.26 rejects Go 1.27's formatting and vice versa — so reformatting the file was not an option in either direction. Controlled comparison, same config and file, pristine checkout, empty cache:
Fix. CI builds the linter from source with
Worth a separate look: the provider's lint job has the same |
| - **Lean comments.** A comment earns its place only by saying what the code cannot — the *why*, a | ||
| trade-off, a non-obvious constraint. Don't restate what a name, type or signature already conveys; | ||
| prefer one sharp line over a paragraph. | ||
| - **Lint only via `task lint`** (golangci-lint, which also enforces gci import ordering and gofmt). |
There was a problem hiding this comment.
d: for gofmt, I'd suggest configuring a hook or something that ensures claude always runs that tool after editing a file. That's a deterministic mechanism that's far more reliable than an instruction in the AGENTS.md.
We have something similar in place in meshfed-release for ktlint.
| - **Stress-test a plan before writing code.** For any non-trivial change, walk each branch of the | ||
| decision tree and settle every open question with a recommended answer first. Catching a wrong turn | ||
| at the plan stage is far cheaper than after the code and tests exist. (*meshcloud-internal*: the | ||
| `grill-me` skill in `meshfed-release/.agents/skills/`.) |
There was a problem hiding this comment.
d: I think the convention we want to rely on is that a developer at meshcloud has the repositories checked out in the same structure as they exist within the meshcloud github org, so meshfed-release would be a sibling directory of meshstack-cli., So, you'd have to refer to it as:
| `grill-me` skill in `meshfed-release/.agents/skills/`.) | |
| `grill-me` skill in `../meshfed-release/.agents/skills/`.) |
There was a problem hiding this comment.
yes, correct. that was missed here! fix it also at other places if necessary.
| CI builds golangci-lint from source with `go.mod`'s Go (`install-mode: goinstall`) instead of | ||
| downloading a release binary, and that is not incidental. The formatters use the `go/format` | ||
| compiled into the linter, so a binary built with a newer Go enforces a different gofmt than the one | ||
| the code is written against — and the two can disagree with no formatting that satisfies both. | ||
| Switching CI to the faster binary install brings that back. |
There was a problem hiding this comment.
d: that's a lot of details about technical plumbing and such, I wonder if this should be shortened. Shouldn't claude opus be sufficiently smart to figure out such details on its own? Just my impression, feel free to disregard if you consider those details useful.
There was a problem hiding this comment.
yes, true. let's shorten it. also investigate tf repo provider instructions if they can be optimized.
Stands up this repository and moves the meshStack API client into it from the Terraform provider, so
the provider and the CLI share one client instead of each carrying its own.
ClickUp: 86cb61rzz, milestone
86cb61we4.
Three commits, meant to be read in order:
feat: bootstrap the meshStack CLI repository— Taskfile, Go-only CI, Nix dev shell, and acobra root command that prints help and nothing else yet.
feat: move the meshStack API client in and add pkg/login—client/and the sharedpkg/login.feat: release with goreleaser and publish a container image— the release path.Layout
cmd/meshstack/package main:main()and the root command. The only main package.cmd/<subcommand>/cmd/buildingblock/list.gowill holdmeshstack buildingblock list.pkg/client/There is no
main.goat the repository root, on purpose: the binary takes its name from itsdirectory, so
go build ./cmd/meshstackandgo install github.com/meshcloud/meshstack-cli/cmd/meshstack@latestboth producemeshstackwith no-oflag, while the module staysmeshstack-cli.cmd/meshstackis the one directory undercmd/that is not a subcommand.
Only the root command exists so far. The convention for the rest is written down in
AGENTS.mdratherthan demonstrated with a stub command that does nothing.
cmd/meshstackwires children in explicitlywith
AddCommand; nothing registers itself frominit().Dependency policy
One external dependency, cobra, and only
cmd/may use it. Everything else is standard library, withtestify in tests.
depguardin.golangci.ymlenforces that per directory — read those rules as thepolicy. It matters because the provider imports
client/andpkg/login, so anything added herelands in the provider's dependency tree and in the public checksum database.
cobra brings
spf13/pflagandinconshreveable/mousetrapwith it as indirect dependencies.Each depguard rule was checked by compiling a file that violates it, because a rule matching no file
is a silent no-op. That is how a pattern bug surfaced:
**/client/**/*.gomatches files insubdirectories of
client/only, never files directly inside it, so most of the client would havebeen unguarded. Both patterns are now listed. The same bug was in the provider's config, where it left
51 client files and all 23
clientmockfiles unchecked — fixed separately and already on theprovider's
main.pkg/loginresolves credentials; it does not run the login exchangeThe exchange already exists in
client/internal/auth.go, where it posts to/api/login, caches theaccess token and refreshes it before expiry. Go's internal rule keeps that code inside
client/, sopkg/loginreaches it throughclient.NewApiKeyAuthorization. A second, hand-rolled exchange wouldhave produced a static token that starts returning 401 once it expires.
Mergeexists for the provider, which merges its provider block attributes overFromEnvso anexplicitly configured attribute outranks the environment.
The four
MESHSTACK_names are exported consts, not private ones. The provider's diagnosticsquote the variable names in their message text, so keeping them private would mean the same strings
living in both repositories.
Releasing
A
v*tag publishes archives and checksums for linux, darwin and windows on amd64 and arm64, thenbuilds the container image for the same tag as a separate job. The image workflow is reusable, so a
push to
mainrefreshes:mainthrough the same path — without that, no usable image would existuntil the first release. Pull requests build the image without pushing it.
Everything published carries the repository name — archives, checksums and
ghcr.io/meshcloud/meshstack-cli— while the binary inside ismeshstack, sodocker run ghcr.io/meshcloud/meshstack-cli buildingblock listreads like the local invocation.No GPG signing: the provider signs because the Terraform registry demands it, which does not apply
here.
Known gap, for later
Authorization.Headertakes aclient/internal.HttpClient, so nothing outsideclient/can call it,and nothing can read the cached token back out. Caching a token in
~/.config/meshstack-cliwillneed a new exported seam.
pkg/loginis the only place that constructs anAuthorization, which iswhere that seam belongs. The read side is already free: a cached token goes back in through
client.NewApiTokenAuthorization, the same pathMESHSTACK_API_TOKENuses.Verification
pkg/loginhas 19 subtests across five table-driven tests. The precedence case wasmutation-checked: it fails on an implementation that prefers key and secret over the token.
golangci-lint: clean.goreleaser checkpasses;goreleaser release --snapshotproducesmeshstack-cli_*.tar.gzcontaining a
meshstackbinary that reports the injected version. The image builds and runs, at4.6 MB.
replace github.com/meshcloud/meshstack-cli => ../meshstack-cli, and its 83 unit tests pass withno failures. That is the gate from the feature plan: nothing was pushed here until the provider
was green against it. Provider PR: feat!: consume the meshStack API client from meshstack-cli terraform-provider-meshstack#284.
Still open
LICENSE, matching the license the client code already carries. collie-cli isApache-2.0, so this is a deliberate pick rather than the house default.
🤖 Generated with Claude Code