Skip to content
Merged
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
2 changes: 1 addition & 1 deletion 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.1
golang.org/x/crypto v0.54.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.37.0
k8s.io/apimachinery v0.37.0
Expand Down Expand Up @@ -168,7 +169,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
23 changes: 21 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,11 @@ 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.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
26 changes: 21 additions & 5 deletions internal/cli/registry/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,27 @@ 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)

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
}
6 changes: 6 additions & 0 deletions internal/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,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
14 changes: 14 additions & 0 deletions internal/node/cloudinit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package node

import (
"fmt"
"strings"
"testing"

Expand All @@ -25,6 +26,10 @@ func testCloudInitData() CloudInitData {
RegistryPort: config.RegistryPort,
RegistryHostname: config.RegistryHostname,
ServiceCIDR: config.ServiceCIDR,

AuthRegistryStaticIP: config.AuthRegistryStaticIP,
Comment thread
HarshwardhanPatil07 marked this conversation as resolved.
AuthRegistryPort: config.AuthRegistryPort,
AuthRegistryHostname: config.AuthRegistryHostname,
}
}

Expand Down Expand Up @@ -117,6 +122,15 @@ func TestRenderTemplate_UserData(t *testing.T) {
if !strings.Contains(s, registryURL) {
t.Errorf("missing registry URL %s", registryURL)
}

authRegistryURL := fmt.Sprintf("%s:%d", config.AuthRegistryStaticIP, config.AuthRegistryPort)
if !strings.Contains(s, authRegistryURL) {
t.Errorf("missing auth registry URL %s", authRegistryURL)
}
authRegistryFQDN := fmt.Sprintf("%s.%s:%d", config.AuthRegistryHostname, config.ClusterDomain, config.AuthRegistryPort)
if !strings.Contains(s, authRegistryFQDN) {
t.Errorf("missing auth registry FQDN %s", authRegistryFQDN)
}
}

func TestValidateYAML(t *testing.T) {
Expand Down
Loading