Release 2026-04-22
Two independent feature tracks, shipped together as a coordinated release across six repos.
Component versions
| Component | Version | Image / Chart |
|---|---|---|
| butler-api | v0.10.0 | Go module |
| butler-controller | v0.15.0 | ghcr.io/butlerdotdev/butler-controller:0.15.0 |
| butler-server | v0.6.0 | ghcr.io/butlerdotdev/butler-server:0.6.0 |
| butler-cli (butleradm, butlerctl) | v0.8.0 | GoReleaser binaries |
| butler-console | v0.6.0 | ghcr.io/butlerdotdev/butler-console:0.6.0 |
| butler-crds chart | 0.13.0 | oci://ghcr.io/butlerdotdev/charts/butler-crds |
| butler-controller chart | 0.11.0 (appVersion 0.15.0) | oci://ghcr.io/butlerdotdev/charts/butler-controller |
| butler-console chart | 0.6.0 (appVersion 0.6.0) | oci://ghcr.io/butlerdotdev/charts/butler-console |
Team environments (ADR-009)
Environments subdivide a team into scoped sub-spaces for clusters. Each environment has an operator-chosen name plus four optional fields (description, limits, access, clusterDefaults). The CLI and console both expose the full model.
What ships
- Env CRUD on a team:
butleradm env create/list/update/delete/migrateand the console's team env page with a 4-section create/edit modal. - Cluster scoping:
butlerctl cluster create --environment <name>and a console env selector. TenantClusters carrybutler.butlerlabs.dev/environment: <name>once scoped. - Per-env quotas:
maxClusters(env-level ceiling),maxClustersPerMember(per-individual cap). - Per-member cap enforcement via the requesting user's identity. butler-server impersonates the caller via
AsUser; butler-cli forwards the authenticated user's email; the admission webhook binds the cap to the real identity via thebutler.butlerlabs.dev/creator-emailannotation. - Additive-only access inheritance: an env's access block can elevate a team member's role but never reduce it. Team admins remain admin everywhere. Env access entries must reference users or groups already present in the team's access block.
- Env-scoped cluster defaults with override-over-team semantics. When a TenantCluster is created in an env, the env's
clusterDefaultsfill any unspecified fields, overriding team-level defaults on conflicts. - Mutation-authority split:
Team.spec.resourceLimitsrequires platform admin.Team.spec.environments[].limitsis team-admin editable (platform admin as well). Enforced by the new Team admission webhook. - Structured 403 passthrough: webhook denials (per-member cap breach, team-admin-only mutation attempt, env-label change without migration annotation) render inline on the console form that triggered them, not as generic toasts. The server forwards the webhook's
messagestring verbatim as a 403 body withreason: "webhook-denied". - Phased migration: existing unlabeled clusters continue to work after a team adds environments.
butleradm env migratebulk-labels them. Directkubectledits to the env label are rejected by the webhook unless thebutler.butlerlabs.dev/migration-operation: "true"annotation is also set, distinguishing intentional migration from stray label changes.
Upgrade notes
Backward-compatible. Teams without spec.environments[] behave identically to pre-release. The new admission webhooks only activate when webhooks are enabled on the controller.
Known limitations
isEnvAdminis a conservative client-side approximation (isTeamAdmin || isPlatformAdmin). When env access elevates an operator to admin, the server enforces it but the console UI still hides admin affordances. Tracked for follow-up pending a server endpoint exposing per-env effective role.- Deferred: env-aware cluster-card menu primitive, drag-and-drop env sections, EnvSwitcher cluster counts, env-list access summary. Filed as
butler-console#41and#42. - The
Client.DefaultNamespacefield in butler-cli's shared client + two display-only sites in context commands still compute"team-" + name; no runtime impact (dead read path onDefaultNamespace, display-only on the others). Filed asbutler-cli#36.
Reference
- Concepts → Environments
- Team CRD reference: EnvironmentSpec, EnvironmentLimits
- butleradm env reference
- butlerctl cluster --environment reference
GitLab GitOps provider and export UX
butler-server's GitOps subsystem now supports gitlab.com and self-hosted GitLab instances alongside GitHub. The console's export flows have been polished across both providers.
What ships
- GitLab provider: full
GitProviderinterface backed bygitlab.com/gitlab-org/api/client-go, registered as"gitlab"in the provider registry. Supports personal access tokens, group-scoped repository listing via the Groups API, merge-request creation, multi-file atomic commits viaCreateCommitactions. - Self-hosted GitLab:
ParseRepoURLparses any host viaurl.Parse(no more hardcodedgithub.com/gitlab.comsubstring matches).GetBranchSHAuses the Commits API withref_nameas a query parameter to avoid nginx%2Fdecode on reverse proxies that sit in front of self-hosted GitLab. URL normalization ensures the API base always ends with/api/v4. - Flux bootstrap provider-aware: butler-server derives the
flux bootstrapsubcommand (gitlabvsgithub), the token environment variable (GITLAB_TOKENvsGITHUB_TOKEN), the--hostnameflag (for self-hosted), and the auth mode (--token-authfor GitLab,--read-write-keyfor GitHub) from the configured provider. - Console UX:
- Custom URL field for both providers with a dynamic token-creation link that respects self-hosted URLs.
- Organization / Group field for scoped repo listing.
SearchableSelectcomponent (keyboard-navigable, portaled) replaces native<select>dropdowns in all repo pickers.- Dynamic branch loading in export modals with text-input fallback.
- Aligned spinner + "Exporting..." UX across every export modal.
Ride-along fixes (shipped on butler-server v0.6.0)
- Audit middleware was silently truncating request bodies at 2 KB via
io.LimitReader; now reads the full body (10 MB cap) and truncates only the audit-log summary. Unblocks migrate requests larger than 2 KB. - Duplicate
namespace.yamlfiles are stripped from subsequent HelmRelease directories inExportAll*Addonsso the generated Kustomize is valid. HelmRelease.Spec.ReleaseNameis now set;HelmRepository.Spec.Type=ociis set foroci://URLs.componentsExtranow plumbs through the management-cluster bootstrap path.- Server read/write/idle timeouts raised to 30s/120s/120s to accommodate long-running GitOps exports.
- Flux deployment discovery generalized from a hardcoded four-name list to a label-filtered list of
flux-systemdeployments; covers future Flux controllers automatically. - Invite URLs now derive
scheme://hostfrom the incoming request viahttpx.PublicBaseURLinstead of the startup-capturedBUTLER_BASE_URL(which defaulted to localhost in dev).
Upgrade notes
Existing GitHub configurations are unchanged. No breaking API changes.
Operational notes
Webhook enforcement is a separate track
The Team and TenantCluster admission webhooks that ADR-009 introduces do not fire against tenant clusters on any production cluster until webhooks are deployed there. On butler-beta post-release, the feature ships in enforcement-dormant state for webhooks: the CLI and console surfaces that create/read/update/delete environments and their clusters are fully usable, but admission-time rejection of disallowed mutations requires the webhook infrastructure track (separate work).
Company 1 pre-deploy tracks still open
Three security and documentation tracks remain open before Company 1 deploy. They are tracked independently and do not block this release:
F-SRV-001: unauthenticated WebSocket terminal path.F-SRV-002: constant-time password compare.F-DOC-001: Apache 2.0 Kamaji attribution.