Skip to content

Commit c5b4bc8

Browse files
author
Michal Minář
committed
image-pruner: Reenable registry-url validation
Signed-off-by: Michal Minář <[email protected]>
1 parent 7785c02 commit c5b4bc8

File tree

3 files changed

+146
-9
lines changed

3 files changed

+146
-9
lines changed

pkg/cmd/admin/prune/images.go

+22-9
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import (
3434
// PruneImagesRecommendedName is the recommended command name
3535
const PruneImagesRecommendedName = "images"
3636

37+
var errNoToken = errors.New("you must use a client config with a token")
38+
3739
var (
3840
imagesLongDesc = templates.LongDesc(`
3941
Remove image stream tags, images, and image layers by age or usage
@@ -57,9 +59,10 @@ var (
5759
conditions holds true:
5860
5961
1. --force-insecure is given
60-
2. user's config allows for insecure connection (the user logged in to the cluster with
61-
--insecure-skip-tls-verify or allowed for insecure connection)
62-
3. registry url is a private or link-local address`)
62+
2. provided registry-url is prefixed with http://
63+
3. registry url is a private or link-local address
64+
4. user's config allows for insecure connection (the user logged in to the cluster with
65+
--insecure-skip-tls-verify or allowed for insecure connection)`)
6366

6467
imagesExample = templates.Examples(`
6568
# See, what the prune command would delete if only images more than an hour old and obsoleted
@@ -74,7 +77,13 @@ var (
7477
%[1]s %[2]s --prune-over-size-limit
7578
7679
# To actually perform the prune operation, the confirm flag must be appended
77-
%[1]s %[2]s --prune-over-size-limit --confirm`)
80+
%[1]s %[2]s --prune-over-size-limit --confirm
81+
82+
# Force the insecure http protocol with the particular registry host name
83+
%[1]s %[2]s --registry-url=http://registry.example.org --confirm
84+
85+
# Force a secure connection with a custom certificate authority to the particular registry host name
86+
%[1]s %[2]s --registry-url=registry.example.org --certificate-authority=/path/to/custom/ca.crt --confirm`)
7887
)
7988

8089
var (
@@ -132,7 +141,7 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri
132141
cmd.Flags().IntVar(opts.KeepTagRevisions, "keep-tag-revisions", *opts.KeepTagRevisions, "Specify the number of image revisions for a tag in an image stream that will be preserved.")
133142
cmd.Flags().BoolVar(opts.PruneOverSizeLimit, "prune-over-size-limit", *opts.PruneOverSizeLimit, "Specify if images which are exceeding LimitRanges (see 'openshift.io/Image'), specified in the same namespace, should be considered for pruning. This flag cannot be combined with --keep-younger-than nor --keep-tag-revisions.")
134143
cmd.Flags().StringVar(&opts.CABundle, "certificate-authority", opts.CABundle, "The path to a certificate authority bundle to use when communicating with the managed Docker registries. Defaults to the certificate authority data from the current user's config file. It cannot be used together with --force-insecure.")
135-
cmd.Flags().StringVar(&opts.RegistryUrlOverride, "registry-url", opts.RegistryUrlOverride, "The address to use when contacting the registry, instead of using the default value. This is useful if you can't resolve or reach the registry (e.g.; the default is a cluster-internal URL) but you do have an alternative route that works.")
144+
cmd.Flags().StringVar(&opts.RegistryUrlOverride, "registry-url", opts.RegistryUrlOverride, "The address to use when contacting the registry, instead of using the default value. This is useful if you can't resolve or reach the registry (e.g.; the default is a cluster-internal URL) but you do have an alternative route that works. Particular transport protocol can be enforced using '<scheme>://' prefix.")
136145
cmd.Flags().BoolVar(&opts.ForceInsecure, "force-insecure", opts.ForceInsecure, "If true, allow an insecure connection to the docker registry that is hosted via HTTP or has an invalid HTTPS certificate. Whenever possible, use --certificate-authority instead of this dangerous option.")
137146

138147
return cmd
@@ -193,12 +202,15 @@ func (o PruneImagesOptions) Validate() error {
193202
if o.KeepTagRevisions != nil && *o.KeepTagRevisions < 0 {
194203
return fmt.Errorf("--keep-tag-revisions must be greater than or equal to 0")
195204
}
196-
if _, err := url.Parse(o.RegistryUrlOverride); err != nil {
205+
if err := imageapi.ValidateRegistryURL(o.RegistryUrlOverride); len(o.RegistryUrlOverride) > 0 && err != nil {
197206
return fmt.Errorf("invalid --registry-url flag: %v", err)
198207
}
199208
if o.ForceInsecure && len(o.CABundle) > 0 {
200209
return fmt.Errorf("--certificate-authority cannot be specified with --force-insecure")
201210
}
211+
if len(o.CABundle) > 0 && strings.HasPrefix(o.RegistryUrlOverride, "http://") {
212+
return fmt.Errorf("--cerificate-authority cannot be specified for insecure http protocol")
213+
}
202214
return nil
203215
}
204216

@@ -274,7 +286,8 @@ func (o PruneImagesOptions) Run() error {
274286

275287
insecure := o.ForceInsecure
276288
if !insecure && len(o.CABundle) == 0 {
277-
insecure = o.ClientConfig.TLSClientConfig.Insecure || netutils.IsPrivateAddress(registryHost)
289+
insecure = o.ClientConfig.TLSClientConfig.Insecure || netutils.IsPrivateAddress(registryHost) ||
290+
strings.HasPrefix(registryHost, "http://")
278291
}
279292

280293
registryClient, err = getRegistryClient(o.ClientConfig, o.CABundle, insecure)
@@ -502,7 +515,7 @@ func getClients(f *clientcmd.Factory) (*client.Client, kclientset.Interface, err
502515
}
503516

504517
if len(clientConfig.BearerToken) == 0 {
505-
return nil, nil, noTokenError
518+
return nil, nil, errNoToken
506519
}
507520

508521
osClient, kubeClient, err := f.Clients()
@@ -523,7 +536,7 @@ func getRegistryClient(clientConfig *restclient.Config, registryCABundle string,
523536
)
524537

525538
if len(token) == 0 {
526-
return nil, noTokenError
539+
return nil, errNoToken
527540
}
528541

529542
if len(registryCABundle) > 0 {

pkg/image/apis/image/helper.go

+42
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ const (
4141
ImportRegistryNotAllowed = "registry is not allowed for import"
4242
)
4343

44+
var errNoRegistryURLPathAllowed = fmt.Errorf("no path after <host>[:<port>] is allowed")
45+
var errNoRegistryURLQueryAllowed = fmt.Errorf("no query arguments are allowed after <host>[:<port>]")
46+
var errRegistryURLHostEmpty = fmt.Errorf("no host name specified")
47+
4448
// DefaultRegistry returns the default Docker registry (host or host:port), or false if it is not available.
4549
type DefaultRegistry interface {
4650
DefaultRegistry() (string, bool)
@@ -1161,3 +1165,41 @@ func (tagref TagReference) HasAnnotationTag(searchTag string) bool {
11611165
}
11621166
return false
11631167
}
1168+
1169+
// ValidateRegistryURL returns error if the given input is not a valid registry URL. The url may be prefixed
1170+
// with http:// or https:// schema. It may not contain any path or query after the host:[port].
1171+
func ValidateRegistryURL(registryURL string) error {
1172+
var (
1173+
u *url.URL
1174+
err error
1175+
parts = strings.SplitN(registryURL, "://", 2)
1176+
)
1177+
1178+
switch len(parts) {
1179+
case 2:
1180+
u, err = url.Parse(registryURL)
1181+
if err != nil {
1182+
return err
1183+
}
1184+
switch u.Scheme {
1185+
case "http", "https":
1186+
default:
1187+
return fmt.Errorf("unsupported scheme: %s", u.Scheme)
1188+
}
1189+
case 1:
1190+
u, err = url.Parse("https://" + registryURL)
1191+
if err != nil {
1192+
return err
1193+
}
1194+
}
1195+
if len(u.Path) > 0 && u.Path != "/" {
1196+
return errNoRegistryURLPathAllowed
1197+
}
1198+
if len(u.RawQuery) > 0 {
1199+
return errNoRegistryURLQueryAllowed
1200+
}
1201+
if len(u.Host) == 0 {
1202+
return errRegistryURLHostEmpty
1203+
}
1204+
return nil
1205+
}

pkg/image/apis/image/helper_test.go

+82
Original file line numberDiff line numberDiff line change
@@ -1833,3 +1833,85 @@ func TestDockerImageReferenceForImage(t *testing.T) {
18331833
t.Errorf("expected failure for unknown image")
18341834
}
18351835
}
1836+
1837+
func TestValidateRegistryURL(t *testing.T) {
1838+
for _, tc := range []struct {
1839+
input string
1840+
expectedError bool
1841+
expectedErrorString string
1842+
}{
1843+
{input: "172.30.30.30:5000"},
1844+
{input: ":5000"},
1845+
{input: "[fd12:3456:789a:1::1]:80/"},
1846+
{input: "[fd12:3456:789a:1::1]:80"},
1847+
{input: "http://172.30.30.30:5000"},
1848+
{input: "http://[fd12:3456:789a:1::1]:5000/"},
1849+
{input: "http://[fd12:3456:789a:1::1]:5000"},
1850+
{input: "http://registry.org:5000"},
1851+
{input: "https://172.30.30.30:5000"},
1852+
{input: "https://:80/"},
1853+
{input: "https://[fd12:3456:789a:1::1]/"},
1854+
{input: "https://[fd12:3456:789a:1::1]"},
1855+
{input: "https://[fd12:3456:789a:1::1]:5000/"},
1856+
{input: "https://[fd12:3456:789a:1::1]:5000"},
1857+
{input: "https://registry.org/"},
1858+
{input: "https://registry.org"},
1859+
{input: "localhost/"},
1860+
{input: "localhost"},
1861+
{input: "localhost:80"},
1862+
{input: "registry.org/"},
1863+
{input: "registry.org"},
1864+
{input: "registry.org:5000"},
1865+
1866+
{
1867+
input: "httpss://registry.org",
1868+
expectedErrorString: "unsupported scheme: httpss",
1869+
},
1870+
{
1871+
input: "ftp://registry.org",
1872+
expectedErrorString: "unsupported scheme: ftp",
1873+
},
1874+
{
1875+
input: "http://registry.org://",
1876+
expectedErrorString: errNoRegistryURLPathAllowed.Error(),
1877+
},
1878+
{
1879+
input: "http://registry.org/path",
1880+
expectedErrorString: errNoRegistryURLPathAllowed.Error(),
1881+
},
1882+
{
1883+
input: "[fd12:3456:789a:1::1",
1884+
expectedError: true,
1885+
},
1886+
{
1887+
input: "bad url",
1888+
expectedError: true,
1889+
},
1890+
{
1891+
input: "/registry.org",
1892+
expectedErrorString: errNoRegistryURLPathAllowed.Error(),
1893+
},
1894+
{
1895+
input: "https:///",
1896+
expectedErrorString: errRegistryURLHostEmpty.Error(),
1897+
},
1898+
{
1899+
input: "http://registry.org?parm=arg",
1900+
expectedErrorString: errNoRegistryURLQueryAllowed.Error(),
1901+
},
1902+
} {
1903+
1904+
err := ValidateRegistryURL(tc.input)
1905+
if err != nil {
1906+
if len(tc.expectedErrorString) > 0 && err.Error() != tc.expectedErrorString {
1907+
t.Errorf("[%s] unexpected error string: %q != %q", tc.input, err.Error(), tc.expectedErrorString)
1908+
} else if len(tc.expectedErrorString) == 0 && !tc.expectedError {
1909+
t.Errorf("[%s] unexpected error: %q", tc.input, err.Error())
1910+
}
1911+
} else if len(tc.expectedErrorString) > 0 {
1912+
t.Errorf("[%s] got non-error while expecting %q", tc.input, tc.expectedErrorString)
1913+
} else if tc.expectedError {
1914+
t.Errorf("[%s] got unexpected non-error", tc.input)
1915+
}
1916+
}
1917+
}

0 commit comments

Comments
 (0)