From a6cbecfabdc827599cf40f4eb882c285f6470cfc Mon Sep 17 00:00:00 2001 From: Roman Berezkin Date: Tue, 14 Jul 2026 13:31:20 +0300 Subject: [PATCH] [mirror] Fix misleading error for registry address without repo path - `d8 mirror push` and `pull --source` now explain what is missing: expected format `registry-host[:port]/path` plus an example built from the user's own input. - Without a path, `name.NewRepository` validates the whole `host:port` as a repository name and blames the `:`, so users read the error as "ports are not allowed". - Remaining parse errors name the argument (``, `--source`) so it is clear which input is bad. - Tests pin the new messages and keep currently-valid addresses accepted (`myregistry/repo`, `localhost:5000//repo`, IP with port). Signed-off-by: Roman Berezkin --- internal/mirror/README.MD | 2 +- internal/mirror/cmd/pull/flags/flags.go | 2 +- internal/mirror/cmd/pull/validation.go | 19 ++++++-- internal/mirror/cmd/pull/validation_test.go | 14 +++++- internal/mirror/cmd/push/validation.go | 13 +++++- internal/mirror/cmd/push/validation_test.go | 49 +++++++++++++++++++++ 6 files changed, 90 insertions(+), 9 deletions(-) diff --git a/internal/mirror/README.MD b/internal/mirror/README.MD index 49b0923a..995bd4b5 100644 --- a/internal/mirror/README.MD +++ b/internal/mirror/README.MD @@ -323,7 +323,7 @@ d8 mirror push [flags] ### Arguments - `` - Path to the directory containing the bundle created by `d8 mirror pull` -- `` - Target registry address (format: `registry-host[:port]/path`) +- `` - Target registry address (format: `registry-host[:port]/path`, e.g. `registry.corp.local:5000/deckhouse`) ### Flags diff --git a/internal/mirror/cmd/pull/flags/flags.go b/internal/mirror/cmd/pull/flags/flags.go index ad581dbf..546cd4e4 100644 --- a/internal/mirror/cmd/pull/flags/flags.go +++ b/internal/mirror/cmd/pull/flags/flags.go @@ -104,7 +104,7 @@ func AddFlags(flagSet *pflag.FlagSet) { &SourceRegistryRepo, "source", EnterpriseEditionRepo, - "Source registry to pull Deckhouse images from.", + "Source registry to pull Deckhouse images from (format: registry-host[:port]/path).", ) flagSet.StringVar( &SourceRegistryLogin, diff --git a/internal/mirror/cmd/pull/validation.go b/internal/mirror/cmd/pull/validation.go index 5e3836be..4f16c127 100644 --- a/internal/mirror/cmd/pull/validation.go +++ b/internal/mirror/cmd/pull/validation.go @@ -66,15 +66,26 @@ func validateSourceRegistry() error { return nil // Default is fine } + source := pullflags.SourceRegistryRepo + + // Check the "host:port" format of the user-provided repository URL. + // It must have a path after the host and port. + if _, repoPath, _ := strings.Cut(source, "/"); repoPath == "" { + return fmt.Errorf( + "--source %q is missing the repository path: expected format registry-host[:port]/path, e.g. %q", + source, strings.TrimRight(source, "/")+"/deckhouse/ee", + ) + } + // We first validate that passed repository reference is correct and can be parsed - if _, err := name.NewRepository(pullflags.SourceRegistryRepo); err != nil { - return fmt.Errorf("Validate registry address: %w", err) + if _, err := name.NewRepository(source); err != nil { + return fmt.Errorf("--source %q is not a valid registry address: %w", source, err) } // Then we parse it as URL to validate that it contains everything we need - registryURL, err := url.ParseRequestURI("docker://" + pullflags.SourceRegistryRepo) + registryURL, err := url.ParseRequestURI("docker://" + source) if err != nil { - return fmt.Errorf("Validate source registry parameter: %w", err) + return fmt.Errorf("Parse --source registry address %q: %w", source, err) } if registryURL.Host == "" { diff --git a/internal/mirror/cmd/pull/validation_test.go b/internal/mirror/cmd/pull/validation_test.go index 9cd32218..68649bf6 100644 --- a/internal/mirror/cmd/pull/validation_test.go +++ b/internal/mirror/cmd/pull/validation_test.go @@ -58,7 +58,19 @@ func TestValidationValidateSourceRegistry(t *testing.T) { name: "invalid registry format - no path", registry: "registry.example.com", expectError: true, - errorMsg: "no registry path", + errorMsg: "is missing the repository path", + }, + { + name: "host with port but no path", + registry: "localhost:5000", + expectError: true, + errorMsg: `--source "localhost:5000" is missing the repository path: expected format registry-host[:port]/path, e.g. "localhost:5000/deckhouse/ee"`, + }, + { + name: "trailing slash without path", + registry: "registry.example.com/", + expectError: true, + errorMsg: "is missing the repository path", }, { name: "invalid registry - malformed", diff --git a/internal/mirror/cmd/push/validation.go b/internal/mirror/cmd/push/validation.go index 7ea4d05b..eb2a6304 100644 --- a/internal/mirror/cmd/push/validation.go +++ b/internal/mirror/cmd/push/validation.go @@ -215,15 +215,24 @@ func parseAndValidateRegistryURLArg(registryArg string) error { return errors.New(" argument is empty") } + // Check the "host:port" format of the user-provided repository URL. + // It must have a path after the host and port. + if _, repoPath, _ := strings.Cut(registry, "/"); repoPath == "" { + return fmt.Errorf( + " %q is missing the repository path (pushing to a registry root is not supported): expected format registry-host[:port]/path, e.g. %q", + registry, strings.TrimRight(registry, "/")+"/deckhouse", + ) + } + // We first validate that passed repository reference is correct and can be parsed if _, err := name.NewRepository(registry); err != nil { - return fmt.Errorf("Validate registry address: %w", err) + return fmt.Errorf(" %q is not a valid registry address: %w", registry, err) } // Then we parse it as URL to validate that it contains everything we need registryURL, err := url.ParseRequestURI("docker://" + registry) if err != nil { - return fmt.Errorf("Validate registry address: %w", err) + return fmt.Errorf("Parse address %q: %w", registry, err) } RegistryHost = registryURL.Host diff --git a/internal/mirror/cmd/push/validation_test.go b/internal/mirror/cmd/push/validation_test.go index 8838add5..b41657a5 100644 --- a/internal/mirror/cmd/push/validation_test.go +++ b/internal/mirror/cmd/push/validation_test.go @@ -453,6 +453,55 @@ func TestParseAndValidateRegistryURLArg(t *testing.T) { name: "no repository path", registry: "registry.example.com", expectError: true, + errorMsg: "is missing the repository path", + }, + { + name: "host with port but no repository path", + registry: "localhost:5000", + expectError: true, + errorMsg: ` "localhost:5000" is missing the repository path (pushing to a registry root is not supported): expected format registry-host[:port]/path, e.g. "localhost:5000/deckhouse"`, + }, + { + name: "scheme is stripped before the missing-path check", + registry: "http://localhost:5000", + expectError: true, + errorMsg: `e.g. "localhost:5000/deckhouse"`, + }, + { + name: "trailing slash without repository path", + registry: "localhost:5000/", + expectError: true, + errorMsg: "is missing the repository path", + }, + { + name: "uppercase repository is a character error, not a missing path", + registry: "localhost:5000/UPPER", + expectError: true, + errorMsg: "can only contain", + }, + { + name: "no registry host", + registry: "/deckhouse/ee", + expectError: true, + errorMsg: "no registry host", + }, + { + name: "IP with port and path", + registry: "192.168.1.1:5000/repo", + wantHost: "192.168.1.1:5000", + wantPath: "/repo", + }, + { + name: "host without dot or colon stays accepted", + registry: "myregistry/repo", + wantHost: "myregistry", + wantPath: "/repo", + }, + { + name: "double slash before repo stays accepted", + registry: "localhost:5000//repo", + wantHost: "localhost:5000", + wantPath: "//repo", }, }