feat: add private Docker registry authentication for base images
All checks were successful
Check / check (pull_request) Successful in 3m34s
All checks were successful
Check / check (pull_request) Successful in 3m34s
Add per-app registry credentials that are passed to Docker during image builds, allowing apps to use base images from private registries. - New registry_credentials table (migration 007) - RegistryCredential model with full CRUD operations - Docker client passes AuthConfigs to ImageBuild when credentials exist - Deploy service fetches app registry credentials before builds - Web UI section for managing registry credentials (add/edit/delete) - Comprehensive unit tests for model and auth config builder - README updated to list the feature
This commit is contained in:
96
internal/docker/auth_test.go
Normal file
96
internal/docker/auth_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package docker //nolint:testpackage // tests unexported buildAuthConfigs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildAuthConfigsEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := buildAuthConfigs(nil)
|
||||
if len(result) != 0 {
|
||||
t.Errorf("expected empty map, got %d entries", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAuthConfigsSingle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auths := []RegistryAuth{
|
||||
{
|
||||
Registry: "registry.example.com",
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
},
|
||||
}
|
||||
|
||||
result := buildAuthConfigs(auths)
|
||||
|
||||
if len(result) != 1 {
|
||||
t.Fatalf("expected 1 entry, got %d", len(result))
|
||||
}
|
||||
|
||||
cfg, ok := result["registry.example.com"]
|
||||
if !ok {
|
||||
t.Fatal("expected registry.example.com key")
|
||||
}
|
||||
|
||||
if cfg.Username != "user" {
|
||||
t.Errorf("expected username 'user', got %q", cfg.Username)
|
||||
}
|
||||
|
||||
if cfg.Password != "pass" {
|
||||
t.Errorf("expected password 'pass', got %q", cfg.Password)
|
||||
}
|
||||
|
||||
if cfg.ServerAddress != "registry.example.com" {
|
||||
t.Errorf("expected server address 'registry.example.com', got %q", cfg.ServerAddress)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAuthConfigsMultiple(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auths := []RegistryAuth{
|
||||
{Registry: "ghcr.io", Username: "ghuser", Password: "ghtoken"},
|
||||
{Registry: "docker.io", Username: "dkuser", Password: "dktoken"},
|
||||
}
|
||||
|
||||
result := buildAuthConfigs(auths)
|
||||
|
||||
if len(result) != 2 {
|
||||
t.Fatalf("expected 2 entries, got %d", len(result))
|
||||
}
|
||||
|
||||
ghcr := result["ghcr.io"]
|
||||
if ghcr.Username != "ghuser" || ghcr.Password != "ghtoken" {
|
||||
t.Errorf("unexpected ghcr.io config: %+v", ghcr)
|
||||
}
|
||||
|
||||
dkr := result["docker.io"]
|
||||
if dkr.Username != "dkuser" || dkr.Password != "dktoken" {
|
||||
t.Errorf("unexpected docker.io config: %+v", dkr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegistryAuthStruct(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auth := RegistryAuth{
|
||||
Registry: "registry.example.com",
|
||||
Username: "testuser",
|
||||
Password: "testpass",
|
||||
}
|
||||
|
||||
if auth.Registry != "registry.example.com" {
|
||||
t.Errorf("expected registry 'registry.example.com', got %q", auth.Registry)
|
||||
}
|
||||
|
||||
if auth.Username != "testuser" {
|
||||
t.Errorf("expected username 'testuser', got %q", auth.Username)
|
||||
}
|
||||
|
||||
if auth.Password != "testpass" {
|
||||
t.Errorf("expected password 'testpass', got %q", auth.Password)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/docker/docker/api/types/image"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
"github.com/docker/go-connections/nat"
|
||||
@@ -105,12 +106,20 @@ func (c *Client) IsConnected() bool {
|
||||
return c.docker != nil
|
||||
}
|
||||
|
||||
// RegistryAuth contains authentication credentials for a Docker registry.
|
||||
type RegistryAuth struct {
|
||||
Registry string
|
||||
Username string
|
||||
Password string //nolint:gosec // credential field required for registry auth
|
||||
}
|
||||
|
||||
// BuildImageOptions contains options for building an image.
|
||||
type BuildImageOptions struct {
|
||||
ContextDir string
|
||||
DockerfilePath string
|
||||
Tags []string
|
||||
LogWriter io.Writer // Optional writer for build output
|
||||
LogWriter io.Writer // Optional writer for build output
|
||||
RegistryAuths []RegistryAuth // Optional registry credentials for pulling private base images
|
||||
}
|
||||
|
||||
// BuildImage builds a Docker image from a context directory.
|
||||
@@ -161,6 +170,21 @@ type PortMapping struct {
|
||||
Protocol string // "tcp" or "udp"
|
||||
}
|
||||
|
||||
// buildAuthConfigs converts RegistryAuth slices into Docker's AuthConfigs map.
|
||||
func buildAuthConfigs(auths []RegistryAuth) map[string]registry.AuthConfig {
|
||||
configs := make(map[string]registry.AuthConfig, len(auths))
|
||||
|
||||
for _, auth := range auths {
|
||||
configs[auth.Registry] = registry.AuthConfig{
|
||||
Username: auth.Username,
|
||||
Password: auth.Password,
|
||||
ServerAddress: auth.Registry,
|
||||
}
|
||||
}
|
||||
|
||||
return configs
|
||||
}
|
||||
|
||||
// buildPortConfig converts port mappings to Docker port configuration.
|
||||
func buildPortConfig(ports []PortMapping) (nat.PortSet, nat.PortMap) {
|
||||
exposedPorts := make(nat.PortSet)
|
||||
@@ -513,12 +537,18 @@ func (c *Client) performBuild(
|
||||
}()
|
||||
|
||||
// Build image
|
||||
resp, err := c.docker.ImageBuild(ctx, tarArchive, dockertypes.ImageBuildOptions{
|
||||
buildOpts := dockertypes.ImageBuildOptions{
|
||||
Dockerfile: opts.DockerfilePath,
|
||||
Tags: opts.Tags,
|
||||
Remove: true,
|
||||
NoCache: false,
|
||||
})
|
||||
}
|
||||
|
||||
if len(opts.RegistryAuths) > 0 {
|
||||
buildOpts.AuthConfigs = buildAuthConfigs(opts.RegistryAuths)
|
||||
}
|
||||
|
||||
resp, err := c.docker.ImageBuild(ctx, tarArchive, buildOpts)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to build image: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user