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
24 changes: 17 additions & 7 deletions api/v1alpha1/localbuild_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,25 @@ type PackageConfigsSpec struct {
CorePackageCustomization map[string]PackageCustomization `json:"packageCustomization,omitempty"`
}

// RegistryMirror defines an external registry mirror configuration
type RegistryMirror struct {
// TargetRegistry is the registry that should be mirrored (e.g., "docker.io", "ghcr.io")
TargetRegistry string `json:"targetRegistry,omitempty"`
// RegistryAddress is the address of the mirror registry (e.g., "http://kind-registry:5000")
RegistryAddress string `json:"registryAddress,omitempty"`
}

// BuildCustomizationSpec fields cannot change once a cluster is created
type BuildCustomizationSpec struct {
Protocol string `json:"protocol,omitempty"`
Host string `json:"host,omitempty"`
IngressHost string `json:"ingressHost,omitempty"`
Port string `json:"port,omitempty"`
UsePathRouting bool `json:"usePathRouting,omitempty"`
SelfSignedCert string `json:"selfSignedCert,omitempty"`
StaticPassword bool `json:"staticPassword,omitempty"`
Protocol string `json:"protocol,omitempty"`
Host string `json:"host,omitempty"`
IngressHost string `json:"ingressHost,omitempty"`
Port string `json:"port,omitempty"`
UsePathRouting bool `json:"usePathRouting,omitempty"`
SelfSignedCert string `json:"selfSignedCert,omitempty"`
StaticPassword bool `json:"staticPassword,omitempty"`
RegistryMirrors []RegistryMirror `json:"registryMirrors,omitempty"`
InsecureRegistryMirrors bool `json:"insecureRegistryMirrors,omitempty"`
}

type LocalbuildSpec struct {
Expand Down
22 changes: 21 additions & 1 deletion api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion pkg/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"os"
"slices"
"time"

"github.com/cnoe-io/idpbuilder/api/v1alpha1"
Expand Down Expand Up @@ -292,5 +293,7 @@ func isBuildCustomizationSpecEqual(s1, s2 v1alpha1.BuildCustomizationSpec) bool
s1.Port == s2.Port &&
s1.UsePathRouting == s2.UsePathRouting &&
s1.SelfSignedCert == s2.SelfSignedCert &&
s1.StaticPassword == s2.StaticPassword
s1.StaticPassword == s2.StaticPassword &&
s1.InsecureRegistryMirrors == s2.InsecureRegistryMirrors &&
slices.Equal(s1.RegistryMirrors, s2.RegistryMirrors)
}
72 changes: 65 additions & 7 deletions pkg/cmd/create/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ const (
extraPackagesUsage = "Paths to locations containing custom packages"
packageCustomizationFilesUsage = "Name of the package and the path to file to customize the core packages with. " +
"valid package names are: argocd, nginx, and gitea. e.g. argocd:/tmp/argocd.yaml"
noExitUsage = "When set, idpbuilder will not exit after all packages are synced. Useful for continuously syncing local directories."
registryMirrorsUsage = "List of registry mirrors in format target=address (e.g. \"docker.io=http://kind-registry:5000,ghcr.io=http://kind-registry:5000\")"
insecureRegistryMirrorsUsage = "When set, configure registry mirrors with insecure TLS verification (skip_verify = true)."
noExitUsage = "When set, idpbuilder will not exit after all packages are synced. Useful for continuously syncing local directories."
)

var (
Expand All @@ -48,6 +50,8 @@ var (
kindConfigPath string
extraPackages []string
registryConfig []string
registryMirrors []string
insecureRegistryMirrors bool
packageCustomizationFiles []string
noExit bool
protocol string
Expand Down Expand Up @@ -78,6 +82,8 @@ func init() {
CreateCmd.PersistentFlags().StringVar(&kindConfigPath, "kind-config", "", kindConfigPathUsage)
CreateCmd.PersistentFlags().StringSliceVar(&registryConfig, "registry-config", []string{}, registryConfigUsage)
CreateCmd.PersistentFlags().Lookup("registry-config").NoOptDefVal = "$XDG_RUNTIME_DIR/containers/auth.json,$HOME/.docker/config.json"
CreateCmd.PersistentFlags().StringSliceVar(&registryMirrors, "registry-mirrors", []string{}, registryMirrorsUsage)
CreateCmd.PersistentFlags().BoolVar(&insecureRegistryMirrors, "insecure-registry-mirrors", false, insecureRegistryMirrorsUsage)

// in-cluster resources related flags
CreateCmd.PersistentFlags().StringVar(&host, "host", globals.DefaultHostName, hostUsage)
Expand Down Expand Up @@ -136,6 +142,11 @@ func create(cmd *cobra.Command, args []string) error {
o[c.Name] = c
}

parsedMirrors, err := parseRegistryMirrors(registryMirrors)
if err != nil {
return err
}

exitOnSync := true
if cmd.Flags().Changed("no-exit") {
exitOnSync = !noExit
Expand All @@ -158,12 +169,14 @@ func create(cmd *cobra.Command, args []string) error {
RegistryConfig: maybeRegistryConfig,

TemplateData: v1alpha1.BuildCustomizationSpec{
Protocol: protocol,
Host: host,
IngressHost: ingressHost,
Port: port,
UsePathRouting: pathRouting,
StaticPassword: devPassword,
Protocol: protocol,
Host: host,
IngressHost: ingressHost,
Port: port,
UsePathRouting: pathRouting,
StaticPassword: devPassword,
RegistryMirrors: parsedMirrors,
InsecureRegistryMirrors: insecureRegistryMirrors,
},

CustomPackageFiles: localFiles,
Expand Down Expand Up @@ -208,6 +221,12 @@ func validate() error {
}

_, _, _, err = helpers.ParsePackageStrings(extraPackages)
if err != nil {
return err
}

// Validate registry mirrors
_, err = parseRegistryMirrors(registryMirrors)
return err
}

Expand Down Expand Up @@ -240,6 +259,45 @@ func getPackageCustomFile(input string) (v1alpha1.PackageCustomization, error) {
}, nil
}

func parseRegistryMirrors(mirrors []string) ([]v1alpha1.RegistryMirror, error) {
var result []v1alpha1.RegistryMirror
for _, mirror := range mirrors {
// Format: target=address (e.g. "docker.io=http://kind-registry:5000")
parts := strings.SplitN(mirror, "=", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid registry mirror format: %s, expected format: target=address (e.g. docker.io=http://kind-registry:5000)", mirror)
}

target := strings.TrimSpace(parts[0])
address := strings.TrimSpace(parts[1])

if target == "" {
return nil, fmt.Errorf("target registry cannot be empty in mirror: %s", mirror)
}
if address == "" {
return nil, fmt.Errorf("registry address cannot be empty in mirror: %s", mirror)
}

// target must be [hostname|ip][:port] (https://github.com/containerd/containerd/blob/main/docs/hosts.md#registry-host-namespace)
parsedTarget, err := url.Parse("https://" + target)
if err != nil || parsedTarget.Host == "" || parsedTarget.Path != "" {
return nil, fmt.Errorf("invalid target registry %q: expected format [hostname|ip][:port] (e.g. docker.io, 192.168.1.1:5000)", target)
}

// address must be a valid URL
parsedURL, err := url.Parse(address)
if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" {
return nil, fmt.Errorf("invalid registry address URL: %s, expected format: http(s)://host:port", address)
}

result = append(result, v1alpha1.RegistryMirror{
TargetRegistry: target,
RegistryAddress: address,
})
}
return result, nil
}

func printSuccessMsg() {
subDomain := "argocd."
subPath := ""
Expand Down
130 changes: 130 additions & 0 deletions pkg/cmd/create/root_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package create

import (
"testing"

"github.com/cnoe-io/idpbuilder/api/v1alpha1"
)

func TestParseRegistryMirrors(t *testing.T) {
type test struct {
name string
input []string
expect []v1alpha1.RegistryMirror
wantErr bool
}

tests := []test{
{
name: "empty input",
input: []string{},
expect: []v1alpha1.RegistryMirror{},
wantErr: false,
},
{
name: "single mirror",
input: []string{"docker.io=http://kind-registry:5000"},
expect: []v1alpha1.RegistryMirror{
{
TargetRegistry: "docker.io",
RegistryAddress: "http://kind-registry:5000",
},
},
wantErr: false,
},
{
name: "multiple mirrors",
input: []string{"docker.io=http://kind-registry:5000", "ghcr.io=http://kind-registry:5000"},
expect: []v1alpha1.RegistryMirror{
{
TargetRegistry: "docker.io",
RegistryAddress: "http://kind-registry:5000",
},
{
TargetRegistry: "ghcr.io",
RegistryAddress: "http://kind-registry:5000",
},
},
wantErr: false,
},
{
name: "mirror with space",
input: []string{" docker.io = http://kind-registry:5000 "},
expect: []v1alpha1.RegistryMirror{
{
TargetRegistry: "docker.io",
RegistryAddress: "http://kind-registry:5000",
},
},
wantErr: false,
},
{
name: "missing equals",
input: []string{"docker.io:http://kind-registry:5000"},
expect: nil,
wantErr: true,
},
{
name: "empty target",
input: []string{"=http://kind-registry:5000"},
expect: nil,
wantErr: true,
},
{
name: "empty address",
input: []string{"docker.io="},
expect: nil,
wantErr: true,
},
{
name: "target with path",
input: []string{"docker.io/library=http://my-registry:5000"},
wantErr: true,
},
{
name: "target with scheme",
input: []string{"https://docker.io=http://my-registry:5000"},
wantErr: true,
},
{
name: "malformed address URL",
input: []string{"docker.io=not-a-url"},
wantErr: true,
},
{
name: "mirror with https",
input: []string{"docker.io=https://my-registry:5000"},
expect: []v1alpha1.RegistryMirror{
{
TargetRegistry: "docker.io",
RegistryAddress: "https://my-registry:5000",
},
},
wantErr: false,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result, err := parseRegistryMirrors(tc.input)
if (err != nil) != tc.wantErr {
t.Errorf("parseRegistryMirrors() error = %v, wantErr %v", err, tc.wantErr)
return
}
if !tc.wantErr {
if len(result) != len(tc.expect) {
t.Errorf("parseRegistryMirrors() got %d mirrors, want %d", len(result), len(tc.expect))
return
}
for i := range result {
if result[i].TargetRegistry != tc.expect[i].TargetRegistry {
t.Errorf("parseRegistryMirrors()[%d].TargetRegistry = %v, want %v", i, result[i].TargetRegistry, tc.expect[i].TargetRegistry)
}
if result[i].RegistryAddress != tc.expect[i].RegistryAddress {
t.Errorf("parseRegistryMirrors()[%d].RegistryAddress = %v, want %v", i, result[i].RegistryAddress, tc.expect[i].RegistryAddress)
}
}
}
})
}
}
17 changes: 17 additions & 0 deletions pkg/controllers/resources/idpbuilder.cnoe.io_localbuilds.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,27 @@ spec:
type: string
ingressHost:
type: string
insecureRegistryMirrors:
type: boolean
port:
type: string
protocol:
type: string
registryMirrors:
items:
description: RegistryMirror defines an external registry mirror
configuration
properties:
registryAddress:
description: RegistryAddress is the address of the mirror
registry (e.g., "http://kind-registry:5000")
type: string
targetRegistry:
description: TargetRegistry is the registry that should
be mirrored (e.g., "docker.io", "ghcr.io")
type: string
type: object
type: array
selfSignedCert:
type: string
staticPassword:
Expand Down
Loading
Loading