Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
214710c
feat: add butleradm bootstrap local for kind-based laptop install
atbagan Jun 10, 2026
4c2acb2
fix: build butler-controller with its own context for local bootstrap
atbagan Jun 10, 2026
b13db64
fix: skip provider controller deploy for local bootstrap
atbagan Jun 10, 2026
7de05cb
fix: skip provider controller wait for local bootstrap
atbagan Jun 10, 2026
11a9d65
chore: sync providerconfig and clusterbootstrap CRDs with local provider
atbagan Jun 10, 2026
996b3ec
fix: satisfy ClusterBootstrap schema for local config
atbagan Jun 10, 2026
cf7adca
fix: use locally built bootstrap image for local bootstrap
atbagan Jun 10, 2026
4b6b774
fix: recreate bootstrap deployment when swapping to local image
atbagan Jun 10, 2026
959a27b
fix: grant bootstrap controller cluster-admin for local in-cluster in…
atbagan Jun 10, 2026
4c7d50f
fix: avoid doubled tag in local butler-controller image
atbagan Jun 11, 2026
f96a382
fix: build local steward image and apply local stability profile
atbagan Jun 11, 2026
7418c79
fix: expose local console on host port 8080 automatically
atbagan Jun 11, 2026
0169b51
fix: grant local console admin user cluster-admin for team management
atbagan Jun 11, 2026
22d3b2e
fix: grant both admin identities cluster-admin on local across server…
atbagan Jun 11, 2026
e17fe9c
fix: simplify local console admin grant to the console login identity
atbagan Jun 11, 2026
f4da4f0
feat: print local console url, username and password at end of bootstrap
atbagan Jun 11, 2026
1c07770
feat: add local provider to the bootstrap TUI wizard
atbagan Jun 11, 2026
7640b39
feat: pull published images by default and harden local bootstrap
atbagan Jun 11, 2026
f164d17
style(tui): use -- instead of em-dashes in wizard help text
atbagan Jun 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/adm/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Example:
}

// Register provider subcommands
cmd.AddCommand(NewLocalCmd(logger))
cmd.AddCommand(NewHarvesterCmd(logger))
cmd.AddCommand(NewNutanixCmd(logger))
cmd.AddCommand(NewGCPCmd(logger))
Expand Down
136 changes: 136 additions & 0 deletions internal/adm/bootstrap/local.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
Copyright 2026 The Butler Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package bootstrap

import (
"os"
"time"

"github.com/butlerdotdev/butler/internal/adm/bootstrap/orchestrator"
"github.com/butlerdotdev/butler/internal/common/log"
"github.com/spf13/cobra"
)

// NewLocalCmd creates the local bootstrap subcommand. It stands up the entire Butler
// platform on a local KIND cluster, with tenant worker nodes running as containers via
// the Cluster API Docker provider (CAPD). No hypervisor or cloud account is required.
func NewLocalCmd(logger *log.Logger) *cobra.Command {
var (
clusterName string
repoRoot string
skipCleanup bool
fromSource bool
)

cmd := &cobra.Command{
Use: "local",
Short: "Bootstrap a Butler management cluster locally on KIND (no hypervisor)",
Long: `Bootstrap a Butler management cluster on a local KIND cluster.

This runs the entire Butler platform on your laptop using Docker and KIND. Tenant
worker nodes run as containers via the Cluster API Docker provider (CAPD), so no
hypervisor or cloud account is required. The KIND cluster is kept after bootstrap so
you can create tenant clusters against it.

By default the published component images are pulled from ghcr.io, so nothing needs
to be built and no source checkout is required.

Prerequisites:
• Docker running locally (at least 4 CPUs and 6 GB of memory)
• kubectl installed

Example:
butleradm bootstrap local

Development:
--from-source builds butler-controller, butler-bootstrap, and steward from the
sibling butlerdotdev repos instead of pulling published images. Use it when
iterating on those components locally.`,
RunE: func(cmd *cobra.Command, args []string) error {
ctx, cancel := setupSignalHandler(cmd, logger)
defer cancel()

if fromSource && repoRoot == "" {
home, _ := os.UserHomeDir()
repoRoot = home + "/code/github.com/butlerdotdev"
}

cfg := localConfig(clusterName)

orch := orchestrator.New(logger, orchestrator.Options{
SkipCleanup: skipCleanup,
Timeout: 30 * time.Minute,
LocalDev: fromSource,
RepoRoot: repoRoot,
})
return orch.Run(ctx, cfg)
},
}

cmd.Flags().StringVar(&clusterName, "name", "butler-local", "management cluster name")
cmd.Flags().BoolVar(&fromSource, "from-source", false, "build component images from the sibling butlerdotdev repos instead of pulling published images (for development)")
cmd.Flags().StringVar(&repoRoot, "repo-root", "", "path to butlerdotdev repos when using --from-source (default: ~/code/github.com/butlerdotdev)")
cmd.Flags().BoolVar(&skipCleanup, "skip-cleanup", false, "no-op for local (the KIND cluster is always kept); accepted for parity")

return cmd
}

// localConfig builds the bootstrap Config for the local provider with laptop defaults:
// single control-plane node, Cilium CNI, MetalLB (pool derived from the kind network),
// CAPI/CAPD for tenant workers, and the locally built butler-controller image. Longhorn
// and kube-vip are intentionally omitted (kind's default StorageClass is used; no HA VIP).
func localConfig(name string) *orchestrator.Config {
if name == "" {
name = "butler-local"
}
return &orchestrator.Config{
Provider: "local",
Cluster: orchestrator.ClusterConfig{
Name: name,
Topology: "single-node",
// CPU/MemoryMB/DiskGB are schema-required VM specs. They are unused for
// the local provider (the KIND node is the control plane, not a VM), but
// must satisfy the ClusterBootstrap CRD minimums.
ControlPlane: orchestrator.NodePoolConfig{
Replicas: 1,
CPU: 2,
MemoryMB: 2048,
DiskGB: 20,
},
},
Network: orchestrator.NetworkConfig{
PodCIDR: "10.244.0.0/16",
ServiceCIDR: "10.96.0.0/12",
// LoadBalancerPool is derived from the kind docker network at runtime.
},
// Talos.Version is unused for local (no Talos nodes) but the CRD requires a
// valid vX.Y.Z value.
Talos: orchestrator.TalosConfig{Version: "v1.9.3"},
Addons: orchestrator.AddonsConfig{
// The management cluster keeps KIND's default CNI (kindnet); installing
// Cilium here would conflict. Tenant clusters get Cilium separately.
CNI: orchestrator.CNIConfig{Type: "none"},
Storage: orchestrator.StorageConfig{Type: "none"},
LoadBalancer: orchestrator.LoadBalancerConfig{Type: "metallb"},
CAPI: orchestrator.CAPIConfig{Version: "v1.9.4"},
// ButlerController image is left at its default (ghcr...butler-controller:latest),
// which matches the image buildAndLoadImages builds and loads for local.
// GetButlerControllerImage appends the version, so setting an image with a tag
// here would produce a doubled :latest:latest reference.
},
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -542,11 +542,10 @@ spec:
type: string
vip:
description: |-
VIP is the virtual IP for the control plane endpoint (kube-vip).
Required for on-prem providers (harvester, nutanix, proxmox).
Optional for cloud providers (gcp, aws, azure) where the first
control plane node IP is used as the endpoint instead.
pattern: ^([0-9]{1,3}\.){3}[0-9]{1,3}$
VIP is the control plane endpoint. For on-prem providers this is
a virtual IP managed by kube-vip. For cloud providers this can be
a load balancer IP or DNS name. Optional for cloud providers where
the first control plane node IP is used as the endpoint instead.
type: string
vipInterface:
description: VIPInterface is the network interface for the VIP
Expand All @@ -568,6 +567,7 @@ spec:
- gcp
- aws
- azure
- local
type: string
providerRef:
description: |-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ spec:
- nutanix: "username", "password"
- proxmox: "username", "password" or "token"
- gcp: "serviceAccountKey" (JSON service account key)
Required for all providers except "local", which needs no credentials.
The per-provider requirement is enforced by the ProviderConfig admission webhook.
properties:
key:
description: |-
Expand Down Expand Up @@ -249,6 +251,18 @@ spec:
minimum: 1
type: integer
type: object
local:
description: |-
Local contains local-provider configuration.
Optional when provider is "local".
properties:
kindNodeImage:
description: |-
KindNodeImage overrides the container image used for tenant worker nodes,
for example "kindest/node:v1.30.0". When empty, the image is derived from
the tenant cluster's Kubernetes version.
type: string
type: object
network:
description: Network configures IPAM and network settings for this
provider.
Expand Down Expand Up @@ -349,6 +363,14 @@ spec:
description: Subnet is the network name for VM placement (e.g.,
"VM Network - VLAN 40").
type: string
timeServers:
description: |-
TimeServers are the default NTP servers for Talos worker nodes on this provider.
Falls back to ButlerConfig.spec.defaultTimeServers if empty.
Required on networks where the Talos default (time.cloudflare.com) is unreachable.
items:
type: string
type: array
type: object
nutanix:
description: |-
Expand Down Expand Up @@ -397,6 +419,7 @@ spec:
- azure
- aws
- gcp
- local
type: string
proxmox:
description: |-
Expand Down Expand Up @@ -474,7 +497,6 @@ spec:
type: string
type: object
required:
- credentialsRef
- provider
type: object
status:
Expand Down
7 changes: 7 additions & 0 deletions internal/adm/bootstrap/manifests/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ func (d *Deployer) DeployControllers(ctx context.Context, provider string) error
return fmt.Errorf("deploying butler-bootstrap: %w", err)
}

// The local provider uses the Cluster API Docker provider (CAPD) for tenant
// workers, installed during the addon phase. There is no Butler provider
// controller to deploy.
if provider == "local" {
return nil
}

// Deploy provider-specific controller
providerFile := fmt.Sprintf("controllers/butler-provider-%s.yaml", provider)
if err := d.deployFile(ctx, Controllers, providerFile); err != nil {
Expand Down
2 changes: 2 additions & 0 deletions internal/adm/bootstrap/orchestrator/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ type ClusterCredentials struct {
Talosconfig []byte
ControlPlaneIPs []string
ConsoleURL string
ConsoleUser string
ConsolePassword string
}

// EventSink receives events from the orchestrator. Implementations must be
Expand Down
Loading
Loading