Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 16 additions & 14 deletions .github/actions/setup-bink/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,6 @@ runs:
shell: bash
run: make build-bink

- name: Build cluster and DNS images
shell: bash
run: |
make build-cluster-image
make build-dns-image

- name: Verify prerequisites
shell: bash
run: |
test -f ./bink
podman images --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}"
df -h /
free -h

- name: Get image digests
id: digests
shell: bash
Expand Down Expand Up @@ -139,3 +125,19 @@ runs:
with:
path: /tmp/podman-image-cache
key: podman-images-v2-${{ inputs.cache-key-prefix }}-${{ steps.digests.outputs.hash }}

# Build checkout-owned images after loading the cache so published images
# cannot replace changes made by the pull request under the same tags.
- name: Build cluster and DNS images
shell: bash
run: |
make build-cluster-image
make build-dns-image

- name: Verify prerequisites
shell: bash
run: |
test -f ./bink
podman images --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}"
df -h /
free -h
1 change: 1 addition & 0 deletions containerfiles/dns/cluster-hosts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
10.88.0.2 registry registry.cluster.local
10.88.0.3 auth-registry auth-registry.cluster.local
7 changes: 1 addition & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/spf13/viper v1.21.0
go.podman.io/common v0.69.1
go.podman.io/podman/v6 v6.1.0
golang.org/x/crypto v0.54.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.36.4
k8s.io/apimachinery v0.36.4
Expand Down Expand Up @@ -75,13 +76,9 @@ require (
github.com/go-openapi/swag/stringutils v0.26.0 // indirect
github.com/go-openapi/swag/typeutils v0.26.0 // indirect
github.com/go-openapi/swag/yamlutils v0.26.0 // indirect
github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 // indirect
github.com/go-openapi/testify/v2 v2.5.1 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-containerregistry v0.21.6 // indirect
Expand Down Expand Up @@ -121,7 +118,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/nxadm/tail v1.4.11 // indirect
github.com/opencontainers/cgroups v0.0.8 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
Expand Down Expand Up @@ -167,7 +163,6 @@ require (
go.podman.io/storage v1.64.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.54.0 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
Expand Down
230 changes: 12 additions & 218 deletions go.sum

Large diffs are not rendered by default.

24 changes: 22 additions & 2 deletions internal/cli/cluster/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ func newStartCmd() *cobra.Command {
var exposePath string
var hostNetworkPopulator bool
var targetImgRef string
var registryUser string
var registryPassword string

cmd := &cobra.Command{
Use: "start",
Expand All @@ -45,7 +47,7 @@ func newStartCmd() *cobra.Command {
bink cluster start --memory 4096 --expose ./kubeconfig`,
RunE: func(cmd *cobra.Command, args []string) error {
logger := logrus.New()
return runStart(cmd.Context(), logger, nodeName, nodeImage, apiPort, memory, maxMemory, exposePath, hostNetworkPopulator, targetImgRef)
return runStart(cmd.Context(), logger, nodeName, nodeImage, apiPort, memory, maxMemory, exposePath, hostNetworkPopulator, targetImgRef, registryUser, registryPassword)
},
}

Expand All @@ -57,11 +59,18 @@ func newStartCmd() *cobra.Command {
cmd.Flags().StringVar(&exposePath, "expose", "", "Expose API and save kubeconfig to PATH after cluster is up")
cmd.Flags().BoolVar(&hostNetworkPopulator, "host-network-populator", false, "Use host networking for the image populator container (fixes DNS in nested podman)")
cmd.Flags().StringVar(&targetImgRef, "target-imgref", "", "Override the bootc image reference tracked by the VM (e.g., registry.cluster.local:5000/node:latest)")
cmd.Flags().StringVar(&registryUser, "registry-user", "", "Username for the authenticated registry")
cmd.Flags().StringVar(&registryPassword, "registry-password", "", "Password for the authenticated registry")

return cmd
}

func runStart(ctx context.Context, logger *logrus.Logger, nodeName string, nodeImage string, apiPort int, memory int, maxMemory int, exposePath string, hostNetworkPopulator bool, targetImgRef string) error {
func runStart(ctx context.Context, logger *logrus.Logger, nodeName string, nodeImage string, apiPort int, memory int, maxMemory int, exposePath string, hostNetworkPopulator bool, targetImgRef string, registryUser string, registryPassword string) error {
authRegistryRequested, err := registry.AuthRegistryRequested(registryUser, registryPassword)
if err != nil {
return fmt.Errorf("invalid auth registry credentials: %w", err)
}

logger.Info("=== Creating Kubernetes cluster ===")
logger.Info("")

Expand All @@ -85,6 +94,11 @@ func runStart(ctx context.Context, logger *logrus.Logger, nodeName string, nodeI
if err := registryMgr.EnsureRegistry(ctx); err != nil {
return fmt.Errorf("ensuring registry: %w", err)
}
if authRegistryRequested {
if err := registryMgr.EnsureAuthRegistry(ctx, registryUser, registryPassword); err != nil {
return fmt.Errorf("ensuring auth registry: %w", err)
}
}
logger.Info("")

logger.Info("Step 3: Ensuring DNS container...")
Expand Down Expand Up @@ -197,6 +211,12 @@ func runStart(ctx context.Context, logger *logrus.Logger, nodeName string, nodeI
logger.Infof(" Push: podman push --tls-verify=false localhost:%d/<image>:<tag>", config.RegistryPort)
logger.Infof(" Pull (in-cluster): %s.%s:%d/<image>:<tag>", config.RegistryHostname, config.ClusterDomain, config.RegistryPort)
logger.Info("")
if authRegistryRequested {
logger.Info("Auth registry (pull with credentials):")
logger.Infof(" Pull (in-cluster): %s.%s:%d/<image>:<tag>", config.AuthRegistryHostname, config.ClusterDomain, config.AuthRegistryPort)
logger.Infof(" Username: %s", registryUser)
logger.Info("")
}

if exposePath != "" {
logger.Info("Step 9: Exposing API server...")
Expand Down
22 changes: 22 additions & 0 deletions internal/cli/cluster/start_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2026 The bink Authors
// SPDX-License-Identifier: Apache-2.0

package cluster

import (
"testing"

. "github.com/onsi/gomega"
)

func TestStartCredentialFlagsDefaultToEmpty(t *testing.T) {
g := NewWithT(t)
cmd := newStartCmd()

username, err := cmd.Flags().GetString("registry-user")
g.Expect(err).ToNot(HaveOccurred())
password, err := cmd.Flags().GetString("registry-password")
g.Expect(err).ToNot(HaveOccurred())
g.Expect(username).To(BeEmpty())
g.Expect(password).To(BeEmpty())
}
2 changes: 1 addition & 1 deletion internal/cli/cluster/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func runStop(ctx context.Context, logger *logrus.Logger, force, removeData bool)
} else {
logger.Info("✅ All cluster data removed")
}
logger.Info("Note: Shared registry (bink-registry) is preserved. Use 'bink registry stop' to remove it.")
logger.Info("Note: Shared registries (bink-registry, bink-auth-registry) are preserved. Use 'bink registry stop' to remove them.")
}

return nil
Expand Down
29 changes: 24 additions & 5 deletions internal/cli/registry/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,30 @@ func newInfoCmd() *cobra.Command {
status = define.ContainerStateRunning.String()
}

fmt.Printf("Registry: %s\n", status)
fmt.Printf("IP: %s\n", info.IP)
fmt.Printf("Host port: %d\n", info.HostPort)
fmt.Printf("Push: podman push --tls-verify=false %s/<image>:<tag>\n", info.PushURL)
fmt.Printf("Pull: %s/<image>:<tag>\n", info.PullURL)
fmt.Printf("Registry (unauthenticated): %s\n", status)
fmt.Printf(" IP: %s\n", info.IP)
fmt.Printf(" Host port: %d\n", info.HostPort)
fmt.Printf(" Push: podman push --tls-verify=false %s/<image>:<tag>\n", info.PushURL)
fmt.Printf(" Pull: %s/<image>:<tag>\n", info.PullURL)
fmt.Println()

authInfo, err := mgr.AuthRegistryInfo(cmd.Context())
if err != nil {
return fmt.Errorf("getting auth registry info: %w", err)
}

authStatus := "stopped"
if authInfo.Running {
authStatus = define.ContainerStateRunning.String()
}

fmt.Printf("Registry (authenticated): %s\n", authStatus)
fmt.Printf(" IP: %s\n", authInfo.IP)
fmt.Printf(" Host port: %d\n", authInfo.HostPort)
fmt.Printf(" Pull: %s/<image>:<tag>\n", authInfo.PullURL)
if authInfo.Username != "" {
fmt.Printf(" Username: %s\n", authInfo.Username)
}

return nil
},
Expand Down
30 changes: 27 additions & 3 deletions internal/cli/registry/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,47 @@ import (
)

func newStartCmd() *cobra.Command {
var authOnly bool
var registryUser string
var registryPassword string

cmd := &cobra.Command{
Use: "start",
Short: "Start the local container registry",
Long: "Start the shared local registry container, creating it if it doesn't exist",
Long: "Start the shared local registry containers, creating them if they don't exist",
RunE: func(cmd *cobra.Command, args []string) error {
authRequested, err := registrypkg.AuthRegistryRequested(registryUser, registryPassword)
if err != nil {
return fmt.Errorf("invalid auth registry credentials: %w", err)
}
if authOnly && !authRequested {
return fmt.Errorf("invalid auth registry credentials: registry username and password are required with --auth")
}

mgr, err := registrypkg.NewManager()
if err != nil {
return fmt.Errorf("creating registry manager: %w", err)
}

if err := mgr.EnsureRegistry(cmd.Context()); err != nil {
return fmt.Errorf("starting registry: %w", err)
if !authOnly {
if err := mgr.EnsureRegistry(cmd.Context()); err != nil {
return fmt.Errorf("starting registry: %w", err)
}
}

if authRequested {
if err := mgr.EnsureAuthRegistry(cmd.Context(), registryUser, registryPassword); err != nil {
return fmt.Errorf("starting auth registry: %w", err)
}
}

return nil
},
}

cmd.Flags().BoolVar(&authOnly, "auth", false, "Start only the authenticated registry")
cmd.Flags().StringVar(&registryUser, "registry-user", "", "Username for the authenticated registry")
cmd.Flags().StringVar(&registryPassword, "registry-password", "", "Password for the authenticated registry")

return cmd
}
32 changes: 32 additions & 0 deletions internal/cli/registry/start_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: 2026 The bink Authors
// SPDX-License-Identifier: Apache-2.0

package registry

import (
"testing"

. "github.com/onsi/gomega"
)

func TestStartAuthFlagsRequireCredentials(t *testing.T) {
g := NewWithT(t)
cmd := newStartCmd()
cmd.SilenceErrors = true
cmd.SilenceUsage = true
cmd.SetArgs([]string{"--auth"})

g.Expect(cmd.Execute()).To(MatchError("invalid auth registry credentials: registry username and password are required with --auth"))
}

func TestStartCredentialFlagsDefaultToEmpty(t *testing.T) {
g := NewWithT(t)
cmd := newStartCmd()

username, err := cmd.Flags().GetString("registry-user")
g.Expect(err).ToNot(HaveOccurred())
password, err := cmd.Flags().GetString("registry-password")
g.Expect(err).ToNot(HaveOccurred())
g.Expect(username).To(BeEmpty())
g.Expect(password).To(BeEmpty())
}
25 changes: 20 additions & 5 deletions internal/cli/registry/stop.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package registry

import (
"errors"
"fmt"

registrypkg "github.com/bootc-dev/bink/internal/registry"
Expand All @@ -12,24 +13,38 @@ import (
)

func newStopCmd() *cobra.Command {
var authOnly bool

cmd := &cobra.Command{
Use: "stop",
Short: "Stop and remove the local registry",
Long: "Stop the shared local registry container and remove its data volume",
Short: "Stop and remove the local registries",
Long: "Stop both local registry containers and remove the shared data volume. Use --auth to stop only the authenticated registry.",
RunE: func(cmd *cobra.Command, args []string) error {
mgr, err := registrypkg.NewManager()
if err != nil {
return fmt.Errorf("creating registry manager: %w", err)
}

if err := mgr.StopRegistry(cmd.Context()); err != nil {
return fmt.Errorf("stopping registry: %w", err)
authErr := mgr.StopAuthRegistry(cmd.Context())
if authOnly {
if authErr != nil {
return fmt.Errorf("stopping auth registry: %w", authErr)
}
logrus.Info("Auth registry stopped and removed")
return nil
}

registryErr := mgr.StopRegistry(cmd.Context())
if err := errors.Join(authErr, registryErr); err != nil {
return fmt.Errorf("stopping registries: %w", err)
}

logrus.Info("Registry stopped and data removed")
logrus.Info("All registries stopped and data removed")
return nil
},
}

cmd.Flags().BoolVar(&authOnly, "auth", false, "Stop only the authenticated registry")

return cmd
}
9 changes: 9 additions & 0 deletions internal/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ const (
LabelKubeadmVersion = "bink.kubeadm-version"
LabelNodeImage = "bink.node-image"

LabelAuthRegistryUser = "bink.auth-registry-user"
LabelAuthRegistryPasswordHash = "bink.auth-registry-password-hash"

ContainerNamePrefix = "k8s-"

DNSContainerName = "dns"
Expand All @@ -76,6 +79,12 @@ const (
RegistryStaticIP = "10.88.0.2"
RegistryHostname = "registry"
RegistryVolume = "bink-registry-data"
RegistryHTTPSecret = "bink-shared-secret"

AuthRegistryContainerName = "bink-auth-registry"
AuthRegistryPort = 5001
AuthRegistryStaticIP = "10.88.0.3"
AuthRegistryHostname = "auth-registry"

HAProxyImage = "docker.io/library/haproxy:lts-alpine"
HAProxyContainerName = "haproxy"
Expand Down
8 changes: 8 additions & 0 deletions internal/node/cloudinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ type CloudInitData struct {
RegistryHostname string
ServiceCIDR string
TargetImgRef string

AuthRegistryStaticIP string
AuthRegistryPort int
AuthRegistryHostname string
}

func (n *Node) newCloudInitData(sshPubKey string) CloudInitData {
Expand All @@ -56,6 +60,10 @@ func (n *Node) newCloudInitData(sshPubKey string) CloudInitData {
RegistryHostname: config.RegistryHostname,
ServiceCIDR: config.ServiceCIDR,
TargetImgRef: n.TargetImgRef,

AuthRegistryStaticIP: config.AuthRegistryStaticIP,
AuthRegistryPort: config.AuthRegistryPort,
AuthRegistryHostname: config.AuthRegistryHostname,
}
}

Expand Down
Loading
Loading