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
73 changes: 69 additions & 4 deletions pkg/types/validation/installconfig.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package validation

import (
"encoding/json"
"fmt"
"net"
"net/url"
Expand Down Expand Up @@ -61,6 +62,10 @@ const hostCryptBypassedAnnotation = "install.openshift.io/hostcrypt-check-bypass
// list of known plugins that require hostPrefix to be set
var pluginsUsingHostPrefix = sets.NewString(string(operv1.NetworkTypeOVNKubernetes))

type imagePullSecret struct {
Auths map[string]map[string]interface{} `json:"auths"`
}

// ValidateInstallConfig checks that the specified install config is valid.
//
//nolint:gocyclo
Expand Down Expand Up @@ -163,11 +168,11 @@ func ValidateInstallConfig(c *types.InstallConfig, usingAgentMethod bool) field.
if c.Proxy != nil {
allErrs = append(allErrs, validateProxy(c.Proxy, c, field.NewPath("proxy"))...)
}
allErrs = append(allErrs, validateImageContentSources(c.DeprecatedImageContentSources, field.NewPath("imageContentSources"))...)
allErrs = append(allErrs, validateImageContentSources(c.DeprecatedImageContentSources, c.PullSecret, field.NewPath("imageContentSources"))...)
if _, ok := validPublishingStrategies[c.Publish]; !ok {
allErrs = append(allErrs, field.NotSupported(field.NewPath("publish"), c.Publish, validPublishingStrategyValues))
}
allErrs = append(allErrs, validateImageDigestSources(c.ImageDigestSources, field.NewPath("imageDigestSources"))...)
allErrs = append(allErrs, validateImageDigestSources(c.ImageDigestSources, c.PullSecret, field.NewPath("imageDigestSources"))...)
if _, ok := validPublishingStrategies[c.Publish]; !ok {
allErrs = append(allErrs, field.NotSupported(field.NewPath("publish"), c.Publish, validPublishingStrategyValues))
}
Expand Down Expand Up @@ -1218,8 +1223,11 @@ func validateProxy(p *types.Proxy, c *types.InstallConfig, fldPath *field.Path)
return allErrs
}

func validateImageContentSources(groups []types.ImageContentSource, fldPath *field.Path) field.ErrorList {
func validateImageContentSources(groups []types.ImageContentSource, pullSecret string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}

var allMirrors []string

for gidx, group := range groups {
groupf := fldPath.Index(gidx)
if err := validateNamedRepository(group.Source); err != nil {
Expand All @@ -1231,13 +1239,19 @@ func validateImageContentSources(groups []types.ImageContentSource, fldPath *fie
allErrs = append(allErrs, field.Invalid(groupf.Child("mirrors").Index(midx), mirror, err.Error()))
continue
}

allMirrors = append(allMirrors, mirror)
}
}
allErrs = append(allErrs, validateMirrorCredentials(allMirrors, pullSecret)...)
return allErrs
}

func validateImageDigestSources(groups []types.ImageDigestSource, fldPath *field.Path) field.ErrorList {
func validateImageDigestSources(groups []types.ImageDigestSource, pullSecret string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}

var allMirrors []string

for gidx, group := range groups {
groupf := fldPath.Index(gidx)
if err := validateNamedRepository(group.Source); err != nil {
Expand All @@ -1249,6 +1263,8 @@ func validateImageDigestSources(groups []types.ImageDigestSource, fldPath *field
allErrs = append(allErrs, field.Invalid(groupf.Child("mirrors").Index(midx), mirror, err.Error()))
continue
}

allMirrors = append(allMirrors, mirror)
}
if group.SourcePolicy != "" {
if len(group.Mirrors) == 0 {
Expand All @@ -1259,6 +1275,7 @@ func validateImageDigestSources(groups []types.ImageDigestSource, fldPath *field
}
}
}
allErrs = append(allErrs, validateMirrorCredentials(allMirrors, pullSecret)...)
return allErrs
}

Expand Down Expand Up @@ -1702,3 +1719,51 @@ func sortedPresenceKeys(presence ipAddressTypeByField) []string {
sort.Strings(keys)
return keys
}

// validateMirrorCredentials checks if mirror registry hosts are present in the pull secret.
func validateMirrorCredentials(mirrors []string, pullSecret string) field.ErrorList {
allErrs := field.ErrorList{}

var ps imagePullSecret
if err := validate.ImagePullSecret(pullSecret); err != nil {
return allErrs
}
if err := json.Unmarshal([]byte(pullSecret), &ps); err != nil {
return allErrs
}

missingHosts := sets.New[string]()
for _, mirror := range mirrors {
mirrorHost, err := extractRegistryHost(mirror)
if err != nil {
continue // Skip if we can't extract the host
}
if _, found := ps.Auths[mirrorHost]; !found {
missingHosts.Insert(mirrorHost)
}
}

for host := range missingHosts {
// Log warnings for registries without credentials
logrus.Warnf("Mirror registry %q is not found in pullSecret", host)
}

return allErrs
}

// extractRegistryHost extracts the registry host (with port if any) from a repository string.
// For example: "registry.example.com:5000/namespace/repo" -> "registry.example.com:5000".
// Returns an error if the repository string cannot be parsed as either a named reference or a host.
func extractRegistryHost(repository string) (string, error) {
ref, err := dockerref.ParseNamed(repository)
if err != nil {
// ErrNameNotCanonical indicates the input is not a fully-qualified repository reference
// (e.g., "registry.example.com:5000" without a path, or short names like "ocp/release").
// In these cases, return the input as-is.
if errors.Is(err, dockerref.ErrNameNotCanonical) {
return repository, nil
}
return "", err
}
return dockerref.Domain(ref), nil
}
147 changes: 147 additions & 0 deletions pkg/types/validation/installconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1641,6 +1641,96 @@ func TestValidateInstallConfig(t *testing.T) {
}(),
expectedError: `cannot set imageContentSources and imageDigestSources at the same time`,
},
{
name: "valid imageContentSources with mirror in pull secret",
installConfig: func() *types.InstallConfig {
c := validInstallConfig()
c.PullSecret = `{"auths":{"mirror.example.com":{"auth":"authorization value"}}}`
c.DeprecatedImageContentSources = []types.ImageContentSource{{
Source: "q.io/ocp/source1",
Mirrors: []string{"mirror.example.com/ocp/release"},
}, {
Source: "q.io/ocp/source2",
Mirrors: []string{"mirror.example.com/ocp/release"},
}}
return c
}(),
},
{
name: "imageContentSources with mirror with port not in pull secret - warning only",
installConfig: func() *types.InstallConfig {
c := validInstallConfig()
c.PullSecret = `{"auths":{"mirror.example.com:5000":{"auth":"authorization value"}}}`
c.DeprecatedImageContentSources = []types.ImageContentSource{{
Source: "q.io/ocp/source1",
Mirrors: []string{"not-in-pullsecret-mirror.example.com:5000/ocp/release"},
}, {
Source: "q.io/ocp/source2",
Mirrors: []string{"not-in-pullsecret-mirror.example.com:5000/ocp/release"},
}}
return c
}(),
},
{
name: "imageContentSources with mirror not in pull secret - warning only",
installConfig: func() *types.InstallConfig {
c := validInstallConfig()
c.PullSecret = `{"auths":{"mirror.example.com":{"auth":"authorization value"}}}`
c.DeprecatedImageContentSources = []types.ImageContentSource{{
Source: "q.io/ocp/source1",
Mirrors: []string{"not-in-pullsecret-mirror.example.com/ocp/release"},
}, {
Source: "q.io/ocp/source2",
Mirrors: []string{"not-in-pullsecret-mirror.example.com/ocp/release"},
}}
return c
}(),
},
{
name: "valid imageDigestSources with mirror in pull secret",
installConfig: func() *types.InstallConfig {
c := validInstallConfig()
c.PullSecret = `{"auths":{"mirror.example.com":{"auth":"authorization value"}}}`
c.ImageDigestSources = []types.ImageDigestSource{{
Source: "q.io/ocp/source1",
Mirrors: []string{"mirror.example.com/ocp/release"},
}, {
Source: "q.io/ocp/source2",
Mirrors: []string{"mirror.example.com/ocp/release"},
}}
return c
}(),
},
{
name: "imageDigestSources with mirror with port not in pull secret - warning only",
installConfig: func() *types.InstallConfig {
c := validInstallConfig()
c.PullSecret = `{"auths":{"mirror.example.com:5000":{"auth":"authorization value"}}}`
c.ImageDigestSources = []types.ImageDigestSource{{
Source: "q.io/ocp/source1",
Mirrors: []string{"not-in-pullsecret-mirror.example.com:5000/ocp/release"},
}, {
Source: "q.io/ocp/source2",
Mirrors: []string{"not-in-pullsecret-mirror.example.com:5000/ocp/release"},
}}
return c
}(),
},
{
name: "imageDigestSources with mirror not in pull secret - warning only",
installConfig: func() *types.InstallConfig {
c := validInstallConfig()
c.PullSecret = `{"auths":{"mirror.example.com":{"auth":"authorization value"}}}`
c.ImageDigestSources = []types.ImageDigestSource{{
Source: "q.io/ocp/source1",
Mirrors: []string{"not-in-pullsecret-mirror.example.com/ocp/release"},
}, {
Source: "q.io/ocp/source2",
Mirrors: []string{"not-in-pullsecret-mirror.example.com/ocp/release"},
}}
return c
}(),
},
{
name: "invalid publishing strategy",
installConfig: func() *types.InstallConfig {
Expand Down Expand Up @@ -2811,6 +2901,63 @@ func TestValidateInstallConfig(t *testing.T) {
}
}

func Test_extractRegistryHost(t *testing.T) {
tests := []struct {
name string
repository string
want string
wantErr bool
}{
{
name: "custom registry with port",
repository: "registry.example.com:5000/namespace/repo",
want: "registry.example.com:5000",
wantErr: false,
},
{
name: "quay.io registry",
repository: "quay.io/openshift/release",
want: "quay.io",
wantErr: false,
},
{
name: "IP address with custom port",
repository: "192.168.1.1:8080/myimage",
want: "192.168.1.1:8080",
wantErr: false,
},
{
name: "single domain name - non-canonical",
repository: "registry.example.com",
want: "registry.example.com",
wantErr: false,
},
{
name: "simple name with namespace - non-canonical",
repository: "ocp/release",
want: "ocp/release",
wantErr: false,
},
{
name: "invalid registry host with leading/trailing hyphens",
repository: "--invalid--/repo",
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := extractRegistryHost(tt.repository)
if tt.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}

func Test_ensureIPv4IsFirstInDualStackSlice(t *testing.T) {
tests := []struct {
name string
Expand Down