Skip to content

feat: add the meshStack CLI and the API client it shares with the Terraform provider - #2

Merged
grubmeshi merged 219 commits into
mainfrom
feature/scaffold-cli
Sep 21, 2026
Merged

grubmeshi merged 219 commits into
mainfrom
feature/scaffold-cli

Conversation

@grubmeshi

@grubmeshi grubmeshi commented Sep 1, 2026 •

Copy link
Copy Markdown
Collaborator

This repository becomes the home of the meshStack API client and of the meshstack binary. The
client moves out of the Terraform provider as a git subtree, so both front ends build against one
client instead of each carrying its own, and the authentication that used to sit inside it becomes
a session both of them resolve the same way. The CLI itself ships with auth login and
auth logout.

flowchart LR
  subgraph cli["meshstack-cli"]
    session["session: settings, profile, credential"]
    client["client/ (git subtree)"]
    session --> client
  end
  binary["meshstack binary"] --> session
  provider["terraform-provider-meshstack"] --> session
  release["meshfed-release"] -->|runs the acceptance suite| binary
  provider -.->|git subtree push / pull| client
Loading

A session answers one question for both front ends: which endpoint, which workspace and which
token this invocation uses. Each setting is declared once, in the package that owns it, and each
front end contributes one source over its own flags or block attributes. A setting takes its value
from the first source that carries one, in a fixed order, while two credentials that resolve at the
same time are an error naming both rather than one quietly winning. A minted token is cached under
a file lock, so concurrent runs share it.

Reading order

The history was rebuilt for commit-by-commit review, so read it in order:

# Commit What it answers
1 import the meshStack API client the subtree add; the second parent is the provider's client history
2 make the imported client a package of this module module path, encoding/json/v2, HTTP and authorization moved out
3 set up the Go module that builds the client toolchain pins, lint gate, release pipeline, shared HTTP/JSON/version packages
4 resolve a meshStack session settings, profiles, credentials, token cache, browser login
5 add the meshstack command line interface the command tree and the auth commands
6 warn when a newer CLI release is on GitHub the once-a-day release check
7 drive the login command against a local meshStack the satellite acceptance suite

Commits 1 and 2 do not build on their own, because go.mod only arrives in commit 3. A
repository bootstrapped in this order cannot avoid that: the subtree add has to land on the empty
root commit for the client's history to come along, and the client's own edits belong next to it
rather than mixed into the module setup. Commit 3 onwards builds, and go build ./... was run on
each.

What a reviewer cannot see

task lint reports no issue and task test passes at the tip. The acceptance suite was not run
here — it needs a live meshStack, which only meshfed-release has.

A few doc comments in client/ are reworded rather than moved unchanged. godoclint and
staticcheck ST1021 reject the provider's original wording, so the change is forced by this
repository's lint configuration.

.github/workflows/test-acceptance.yml still lists feature/scaffold-cli as a push trigger. A
pull_request_target run reads the workflow from the base branch, where the file does not exist
yet, so a push is the only trigger that can try the dispatcher out. Delete that line before merge.

Paired by branch name with meshcloud/meshfed-release#10866, whose credentials the acceptance suite
reads, and with meshcloud/terraform-provider-meshstack#299, which consumes this.

ClickUp: 86cb61rzz, milestone
86cb61we4.

🤖 Generated with Claude Code

henryde and others added 30 commits June 20, 2024 13:49
meshStack enforces the Accept header soon, so we have to make sure to always provide it
includes adaptations from PR remarks
@meshcloud-gh-actions

meshcloud-gh-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Coverage of the acceptance run against the meshStack backend, on b8e05c384f8379d3c634647b7c2bd27e101f5554.

Scope Coverage
Unit tests 53.6%
Acceptance tests 15.1%
Combined 54.0%
Uncovered functions
client/api_key.go:44: newApiKeyClient 0.0%
client/api_key.go:48: meshApiKeyClient.Create 0.0%
client/api_key.go:52: meshApiKeyClient.Read 0.0%
client/api_key.go:56: meshApiKeyClient.Update 0.0%
client/api_key.go:60: meshApiKeyClient.Delete 0.0%
client/api_key_permissions.go:21: ApiKeyPermissions.AllCodes 0.0%
client/api_key_permissions.go:34: ApiKeyPermissions.WorkspaceCodes 0.0%
client/api_key_permissions.go:50: ApiKeyPermissions.MarkdownString 0.0%
client/api_key_permissions.go:226: AllApiKeyPermissions 0.0%
client/api_key_permissions.go:231: WorkspacePermissionCodes 0.0%
client/building_block_definition.go:72: MeshBuildingBlockDefinitionApprovalPolicies.NothingRequiresApproval 0.0%
client/building_block_definition.go:83: DisabledSchedule 0.0%
client/building_block_definition.go:90: MeshBuildingBlockDefinitionSchedule.IsDisabled 0.0%
client/building_block_definition.go:95: MeshBuildingBlockDefinitionSpec.HasNeutralPolicies 0.0%
client/building_block_definition.go:100: MeshBuildingBlockDefinitionSpec.WithNeutralPolicies 0.0%
client/building_block_definition.go:139: newBuildingBlockDefinitionClient 0.0%
client/building_block_definition.go:153: meshBuildingBlockDefinitionClient.List 0.0%
client/building_block_definition.go:160: meshBuildingBlockDefinitionClient.Read 0.0%
client/building_block_definition.go:164: meshBuildingBlockDefinitionClient.Create 0.0%
client/building_block_definition.go:168: meshBuildingBlockDefinitionClient.Update 0.0%
client/building_block_definition.go:172: meshBuildingBlockDefinitionClient.Delete 0.0%
client/building_block_definition_version.go:100: TagInputTargetsFor 0.0%
client/building_block_definition_version.go:235: newBuildingBlockDefinitionVersionClient 0.0%
client/building_block_definition_version.go:245: meshBuildingBlockDefinitionVersionClient.List 0.0%
client/building_block_definition_version.go:251: meshBuildingBlockDefinitionVersionClient.Create 0.0%
... and 200 more

covdata func names a method without its receiver, so an entry can belong to an implementation nothing selects rather than to a function the tests never reached. Open the file and line before reading one as a coverage gap.

The client arrives as a git subtree rather than a copy, so its history comes
along and a change made here can travel back with `git subtree push`. The
subtree's history carries the files at the repository root, which is why a
later pull needs a `git subtree split` of the provider first.

CU-86cb61rzz

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

git-subtree-dir: client
git-subtree-mainline: 8f6cc2f
git-subtree-split: cf78ea1
@grubmeshi
grubmeshi marked this pull request as ready for review September 18, 2026 20:29
@grubmeshi
grubmeshi requested a balanced review from Copilot September 18, 2026 20:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Credential leakage, workflow injection, setting-resolution defects, and panic paths remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Introduces the meshStack CLI and shared Go API client, including unified authentication, profile handling, release automation, and acceptance-test integration.

Changes:

  • Adds the meshstack command with login/logout and session resolution.
  • Imports and modernizes the shared meshStack API client.
  • Adds build, packaging, container, CI, and repository infrastructure.
File summaries
File Description
Taskfile.yml Defines development and release tasks.
README.md Documents installation and development.
pkg/setting/source.go Exposes setting-source helpers.
pkg/setting/setting.go Exposes configurable settings.
pkg/profile/profile.go Exposes profile resolution.
pkg/io/stderr.go Exposes contextual stderr handling.
pkg/auth/session.go Exposes sessions and clients.
pkg/auth/method.go Exposes authentication methods.
meshstack-satellite.gradle Configures acceptance-test execution.
internal/version/version.go Implements version parsing and comparison.
internal/testutil/jsontest/jsontest.go Adds JSON test helper.
internal/setting/source.go Defines setting sources.
internal/setting/setting.go Defines typed settings and parsers.
internal/setting/setting_test/setting.go Adds setting test source.
internal/setting/setting_test.go Tests boolean parsing.
internal/setting/resolve.go Implements source precedence.
internal/setting/env.go Implements environment lookup.
internal/profile/testdata/jwt.json Adds profile JWT fixture.
internal/profile/testdata/configdir/profiles.json Adds profile fixture.
internal/profile/testdata/configdir/credentials/dev-local.json Adds credential fixture.
internal/profile/testdata/configdir/credentials-cache/dev-local/apiKey.json Adds token-cache fixture.
internal/profile/testdata/configdir/.gitignore Ignores fixture lock artifacts.
internal/profile/resolve.go Resolves or creates profiles.
internal/profile/profiles.go Loads and stores profiles.
internal/profile/profile.go Defines profile behavior.
internal/profile/name.go Defines profile names and help.
internal/oidc/scope/scope.go Models OIDC scopes.
internal/oidc/jwt/testdata/jwt_unscoped.json Adds unscoped JWT fixture.
internal/oidc/jwt/testdata/jwt_unscoped_no_exp.json Adds no-expiry JWT fixture.
internal/oidc/jwt/testdata/jwt_scoped.json Adds scoped JWT fixture.
internal/oidc/jwt/testdata/jwt_opaque.json Adds opaque-token fixture.
internal/oidc/jwt/testdata/jwt_not_json.json Adds malformed-payload fixture.
internal/oidc/jwt/testdata/jwt_not_base64.json Adds invalid-base64 fixture.
internal/oidc/jwt/jwt.go Parses JWT payloads.
internal/oidc/jwt/jwt_test.go Tests JWT parsing and claims.
internal/oidc/jwt/claim.go Defines typed JWT claims.
internal/oidc/client.go Implements OIDC token operations.
internal/oidc/browser/open_windows.go Opens browsers on Windows.
internal/oidc/browser/open_linux.go Opens browsers on Linux.
internal/oidc/browser/open_darwin.go Opens browsers on macOS.
internal/oidc/browser/callback.html Defines login callback page.
internal/oidc/browser/callback.go Renders callback responses.
internal/oidc/authcode.go Implements PKCE authorization flow.
internal/meshstack/workspaces.go Models selectable workspaces.
internal/meshstack/workspace.go Defines workspace settings.
internal/meshstack/settings.go Defines endpoint/version settings.
internal/lock/readonly_windows.go Detects Windows read-only errors.
internal/lock/readonly_other.go Detects Unix read-only errors.
internal/lock/lock.go Implements process-safe locking.
internal/json/unmarshal.go Adds JSON file decoding.
internal/json/marshal.go Adds atomic JSON persistence.
internal/json/marshal_test.go Tests persisted permissions.
internal/io/stderr.go Stores stderr in context.
internal/http/retry_test.go Tests retry backoff behavior.
internal/http/method.go Re-exports HTTP methods.
internal/http/logging.go Formats HTTP debug logs.
internal/http/logging_test.go Tests authorization redaction.
internal/http/logging_internal_test.go Tests deferred log rendering.
internal/http/http_error.go Defines HTTP response errors.
internal/http/auth.go Adds authorized request retries.
internal/config/directory.go Resolves configuration paths.
internal/auth/workspace_test.go Tests credential context propagation.
internal/auth/version_check.go Checks for newer releases.
internal/auth/credentials.go Resolves authentication credentials.
internal/auth/credential/oidclogin_test.go Tests OIDC scope selection.
internal/auth/credential/name.go Defines credential names.
internal/auth/credential/manual.go Implements static-token credentials.
internal/auth/credential/identity.go Hashes credential identities.
internal/auth/credential/credential.go Defines credential storage types.
internal/auth/credential/cache.go Manages in-memory credential caches.
internal/auth/credential/apikey.go Implements API-key authentication.
internal/auth/credential_oidclogin.go Resolves browser credentials.
internal/auth/credential_manual.go Resolves manual tokens.
internal/auth/credential_apikey.go Resolves API-key settings.
internal/auth/auth.go Refreshes and caches bearer tokens.
infra/github/terraform.tf Configures GitHub Terraform provider.
infra/github/main.tf Defines repository protections.
flake.nix Defines Nix builds and shell.
flake.lock Pins Nix dependencies.
Dockerfile Builds the runtime image.
cmd/meshstack/meshstack.go Defines the CLI root command.
cmd/internal/version.go Resolves CLI build version.
cmd/internal/testacc/credentials_test.go Tests credential resolution acceptance paths.
cmd/internal/session.go Connects CLI flags to sessions.
cmd/internal/run.go Adds timeout and signal handling.
cmd/internal/flags.go Defines shared CLI flags.
cmd/internal/client.go Resolves command API clients.
cmd/auth/logout.go Adds logout command.
cmd/auth/auth.go Adds authentication command group.
client/workspace.go Adds workspace API client.
client/workspace_user_binding.go Adds workspace-user bindings.
client/workspace_group_binding.go Adds workspace-group bindings.
client/workspace_binding.go Defines workspace binding DTOs.
client/types/xurl/url.go Adds validated URL type.
client/types/variant/variant.go Adds polymorphic JSON values.
client/types/clienttypes.go Defines shared client types.
client/types/clienttypes_test.go Tests set-type detection.
client/testdata/building_block_definition_version_input/sensitive.json Adds sensitive-input fixture.
client/testdata/building_block_definition_version_input/sensitive_but_no_hash.json Adds invalid-sensitive fixture.
client/testdata/building_block_definition_version_input/not_sensitive.json Adds ordinary-input fixture.
client/testdata/building_block_definition_version_input/not_sensitive_but_hash.json Adds hash-shaped input fixture.
client/testdata/building_block_definition_version_input/empty.json Adds empty-input fixture.
client/tenant_v4_deletion_test.go Tests tenant deletion states.
client/tag_definition.go Adds tag-definition API client.
client/service_instance.go Adds service-instance API client.
client/refs.go Defines meshObject references.
client/project.go Adds project API client.
client/project_user_binding.go Adds project-user bindings.
client/project_group_binding.go Adds project-group bindings.
client/project_binding.go Defines project binding DTOs.
client/platform_type.go Adds platform-type API client.
client/platform_properties_openshift.go Defines OpenShift properties.
client/platform_properties_kubernetes.go Defines Kubernetes properties.
client/platform_properties_gcp.go Defines GCP properties.
client/platform_properties_custom.go Defines custom-platform properties.
client/platform_properties_azurerg.go Defines Azure RG properties.
client/platform_properties_azure.go Defines Azure properties.
client/platform_properties_aws.go Defines AWS properties.
client/platform_properties_aks.go Defines AKS properties.
client/platform_config_openshift.go Defines OpenShift configuration.
client/platform_config_kubernetes.go Defines Kubernetes configuration.
client/platform_config_gcp.go Defines GCP configuration.
client/platform_config_custom.go Defines custom-platform configuration.
client/platform_config_azurerg.go Defines Azure RG configuration.
client/platform_config_azure.go Defines Azure configuration.
client/platform_config_aks.go Defines AKS configuration.
client/payment_method.go Adds payment-method API client.
client/mesh_info.go Adds backend information client.
client/location.go Adds location API client.
client/integration.go Adds integration API client.
client/integration_config.go Defines integration configurations.
client/client.go Composes the shared API client.
client/client_kind.go Defines meshObject kind constants.
client/client_kind_test.go Tests inferred object kinds.
client/buildingblock.go Adds legacy building-block client.
client/building_block_runner.go Adds runner API client.
client/building_block_run.go Adds run-log API client.
client/building_block_definition_version_test.go Tests definition input decoding.
client/api_key.go Adds API-key resource client.
.goreleaser.yml Configures release artifacts.
.gitignore Ignores generated and local files.
.github/workflows/test-acceptance.yml Dispatches backend acceptance tests.
.github/workflows/release.yml Publishes tagged releases.
.github/workflows/build-image.yml Builds and publishes container images.
.dockerignore Minimizes Docker build context.
.claude/settings.json Formats edited Go files.
Review details

Suppressed comments (1)

client/types/clienttypes_test.go:28

  • This case is labeled Set[int] but passes Set[string] again, leaving the integer instantiation untested.
  • Files reviewed: 180/183 changed files
  • Comments generated: 10
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/workflows/build-image.yml Outdated
Comment thread internal/http/logging.go
Comment thread client/integration.go
Comment thread cmd/internal/flags.go
Comment thread cmd/internal/flags.go
Comment thread internal/profile/profiles.go
Comment thread internal/setting/resolve.go
Comment thread .github/workflows/test-acceptance.yml
Comment thread client/types/clienttypes_test.go Outdated
Comment thread internal/profile/name.go Outdated
The client keeps its public shape, and a caller that held a `client.Client`
still does. What changes is where it gets its parts from: the HTTP client, the
request options and the retry policy move one directory up into
`internal/http`, so that `internal/oidc` and `pkg/auth` can use them too — Go's
internal rule closes `client/internal` to both. Authorization becomes an
interface the client is handed rather than a login it performs itself, which is
what lets one resolved session decide the endpoint and the token together.

Bodies marshal with `encoding/json/v2`, and the `apiVersion` and `kind` members
now come from an `,embed` tag rather than from a hand-written wrapper struct
per request.

This commit does not build on its own: `go.mod` arrives in the next one.

CU-86cb61rzz

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@grubmeshi
grubmeshi force-pushed the feature/scaffold-cli branch 6 times, most recently from 2ac7bf6 to 3c68e98 Compare September 19, 2026 04:55
@grubmeshi
grubmeshi requested a review from henryde September 19, 2026 05:04
@grubmeshi
grubmeshi force-pushed the feature/scaffold-cli branch 2 times, most recently from 7750330 to 4b9a456 Compare September 21, 2026 09:19
grubmeshi and others added 8 commits September 21, 2026 11:26
The repository becomes a Go module, with the toolchain pinned in go.mod and in
the nix flake, a lint gate that also formats, a release pipeline for the
archives and the container image, and the GitHub ruleset under terraform.

The HTTP, JSON and version packages the client compiles against come along in
this commit rather than a later one, so that the tree builds from here on. They
sit outside `client/` because the login flow and the settings layer need them
as well, and Go's internal rule would close `client/internal` to both.

CU-86cb61rzz

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ials

Both front ends need the same answer to one question: which endpoint, which
workspace and which token does this invocation use. A session resolves that
once, from the environment, a stored profile and whatever the front end itself
offers, and hands out a client that already carries the result. Each setting is
declared once, in the package that owns it, so a message naming an environment
variable is written where the variable is defined.

A credential is resolved by name. Two sources that carry different values for
the same setting raise an error that names both, rather than one quietly
winning. A minted token is cached in the config directory under a file lock, so
concurrent runs share one token instead of each asking for its own, and the
browser login refreshes before expiry rather than sending the person back to
keycloak.

CU-86cb61rzz

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`meshstack auth login` opens a browser, lets the person pick a workspace and
writes the profile and its cached token to the config directory;
`meshstack auth logout` ends that session and drops the token. `meshstack
login` and `meshstack logout` are the same commands registered a second time at
the top level, because cobra's aliases only rename a command within its parent.

The command tree is the CLI's one settings source: every flag it defines
resolves through the same mechanism the environment and the stored profile go
through, so a flag and an environment variable that disagree produce the same
error here as anywhere else.

CU-86cb61rzz

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A person who installed the binary once has no other way to learn that a release
fixed the thing they are hitting. The check runs at most once a day, remembers
the answer in the config directory, and never fails the command it rides on: a
GitHub outage or an offline machine leaves the invocation alone.

It sits beside the session rather than in its own package because the release
lookup needs the shared HTTP client, and the client's dependency rules keep
that reachable only from here.

CU-86cb61rzz

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The browser login is the one flow no unit test reaches: it needs a real
keycloak to answer the authorization request and a real meshStack to accept
the token that comes back. This suite builds the binary, plays the browser
against keycloak's HTML forms with its own cookie jar, and then checks that
the profile and the cached token it wrote let a second invocation run without
logging in again.

meshfed-release runs it as a satellite suite, because a whole meshStack only
exists there. It skips without one, so `go test ./...` stays green here.

CU-86cb61rzz

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The profile setting becomes part of the surface the Terraform provider imports, which declares a
profile block attribute from it, so the declaration stays in the package that owns profiles and each
front end only contributes a source. As a front end source the flag outranks MESHSTACK_PROFILE, the
profile matching the endpoint and the last selected profile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Profiles were only ever created on first-time use, when there is no configuration file at all, so a
name other than the default named a profile that could never come to exist and --profile had almost
nothing to select. A login is the command that writes a profile, so it is the one that may create
one; for every other command an unknown name is a typo, and an empty profile written from it would
hide the mistake instead of reporting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Terraform provider contributes setting sources but holds no session, so it cannot ask which
credential a run authenticates with. It needs that to look a workspace up only for a browser login,
where a workspace is what scopes the token, and the context carrying the workspaces to pick from is
where the answer already belongs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@grubmeshi
grubmeshi merged commit b8e05c3 into main Sep 21, 2026
6 checks passed
@grubmeshi
grubmeshi deleted the feature/scaffold-cli branch September 21, 2026 09:57
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.