Skip to main content

Release 2026-04-22

Two independent feature tracks, shipped together as a coordinated release across six repos.

Component versions

ComponentVersionImage / Chart
butler-apiv0.10.0Go module
butler-controllerv0.15.0ghcr.io/butlerdotdev/butler-controller:0.15.0
butler-serverv0.6.0ghcr.io/butlerdotdev/butler-server:0.6.0
butler-cli (butleradm, butlerctl)v0.8.0GoReleaser binaries
butler-consolev0.6.0ghcr.io/butlerdotdev/butler-console:0.6.0
butler-crds chart0.13.0oci://ghcr.io/butlerdotdev/charts/butler-crds
butler-controller chart0.11.0 (appVersion 0.15.0)oci://ghcr.io/butlerdotdev/charts/butler-controller
butler-console chart0.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/migrate and 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 carry butler.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 the butler.butlerlabs.dev/creator-email annotation.
  • 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 clusterDefaults fill any unspecified fields, overriding team-level defaults on conflicts.
  • Mutation-authority split: Team.spec.resourceLimits requires platform admin. Team.spec.environments[].limits is 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 message string verbatim as a 403 body with reason: "webhook-denied".
  • Phased migration: existing unlabeled clusters continue to work after a team adds environments. butleradm env migrate bulk-labels them. Direct kubectl edits to the env label are rejected by the webhook unless the butler.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

  • isEnvAdmin is 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#41 and #42.
  • The Client.DefaultNamespace field in butler-cli's shared client + two display-only sites in context commands still compute "team-" + name; no runtime impact (dead read path on DefaultNamespace, display-only on the others). Filed as butler-cli#36.

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 GitProvider interface backed by gitlab.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 via CreateCommit actions.
  • Self-hosted GitLab: ParseRepoURL parses any host via url.Parse (no more hardcoded github.com/gitlab.com substring matches). GetBranchSHA uses the Commits API with ref_name as a query parameter to avoid nginx %2F decode 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 bootstrap subcommand (gitlab vs github), the token environment variable (GITLAB_TOKEN vs GITHUB_TOKEN), the --hostname flag (for self-hosted), and the auth mode (--token-auth for GitLab, --read-write-key for 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.
    • SearchableSelect component (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.yaml files are stripped from subsequent HelmRelease directories in ExportAll*Addons so the generated Kustomize is valid.
  • HelmRelease.Spec.ReleaseName is now set; HelmRepository.Spec.Type=oci is set for oci:// URLs.
  • componentsExtra now 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-system deployments; covers future Flux controllers automatically.
  • Invite URLs now derive scheme://host from the incoming request via httpx.PublicBaseURL instead of the startup-captured BUTLER_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.

References