Skip to content

Commit afb8ebb

Browse files
Merge pull request #16138 from miminar/origin3.6-secure-image-prune
Automatic merge from submit-queue [3.6][Backport] Prune images (not)securely Back-porting: - #14114 - #14405 - #14914 - #15899 Resolves [bz#1476779](https://bugzilla.redhat.com/show_bug.cgi?id=1476779)
2 parents b216da3 + b4bb2f7 commit afb8ebb

File tree

9 files changed

+1083
-545
lines changed

9 files changed

+1083
-545
lines changed

pkg/cmd/admin/prune/images.go

Lines changed: 109 additions & 60 deletions
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
@@ -53,11 +55,14 @@ var (
5355
authority other than the one present in current user's config, you may need to specify it
5456
using --certificate-authority flag.
5557
56-
Insecure connection is allowed in following cases unless certificate-authority is specified:
57-
1. --force-insecure is given
58-
2. user's config allows for insecure connection (the user logged in to the cluster with
59-
--insecure-skip-tls-verify or allowed for insecure connection)
60-
3. registry url is not given or it's a private/link-local address`)
58+
Insecure connection is allowed if certificate-authority is not specified and one of the following
59+
conditions holds true:
60+
61+
1. --force-insecure is given
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)`)
6166

6267
imagesExample = templates.Examples(`
6368
# See, what the prune command would delete if only images more than an hour old and obsoleted
@@ -72,7 +77,13 @@ var (
7277
%[1]s %[2]s --prune-over-size-limit
7378
7479
# To actually perform the prune operation, the confirm flag must be appended
75-
%[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`)
7687
)
7788

7889
var (
@@ -93,11 +104,10 @@ type PruneImagesOptions struct {
93104
Namespace string
94105
ForceInsecure bool
95106

96-
OSClient client.Interface
97-
KClient kclientset.Interface
98-
RegistryClient *http.Client
99-
Out io.Writer
100-
Insecure bool
107+
ClientConfig *restclient.Config
108+
OSClient client.Interface
109+
KubeClient kclientset.Interface
110+
Out io.Writer
101111
}
102112

103113
// NewCmdPruneImages implements the OpenShift cli prune images command.
@@ -131,7 +141,7 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri
131141
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.")
132142
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.")
133143
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.")
134-
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.")
135145
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.")
136146

137147
return cmd
@@ -169,18 +179,14 @@ func (o *PruneImagesOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command,
169179
if err != nil {
170180
return err
171181
}
182+
o.ClientConfig = clientConfig
172183

173-
o.Insecure = o.ForceInsecure
174-
if !o.Insecure && len(o.CABundle) == 0 {
175-
o.Insecure = clientConfig.TLSClientConfig.Insecure || len(o.RegistryUrlOverride) == 0 || netutils.IsPrivateAddress(o.RegistryUrlOverride)
176-
}
177-
osClient, kClient, registryClient, err := getClients(f, o.CABundle, o.Insecure)
184+
osClient, kubeClient, err := getClients(f)
178185
if err != nil {
179186
return err
180187
}
181188
o.OSClient = osClient
182-
o.KClient = kClient
183-
o.RegistryClient = registryClient
189+
o.KubeClient = kubeClient
184190

185191
return nil
186192
}
@@ -196,12 +202,15 @@ func (o PruneImagesOptions) Validate() error {
196202
if o.KeepTagRevisions != nil && *o.KeepTagRevisions < 0 {
197203
return fmt.Errorf("--keep-tag-revisions must be greater than or equal to 0")
198204
}
199-
if _, err := url.Parse(o.RegistryUrlOverride); err != nil {
205+
if err := imageapi.ValidateRegistryURL(o.RegistryUrlOverride); len(o.RegistryUrlOverride) > 0 && err != nil {
200206
return fmt.Errorf("invalid --registry-url flag: %v", err)
201207
}
202208
if o.ForceInsecure && len(o.CABundle) > 0 {
203209
return fmt.Errorf("--certificate-authority cannot be specified with --force-insecure")
204210
}
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+
}
205214
return nil
206215
}
207216

@@ -217,12 +226,12 @@ func (o PruneImagesOptions) Run() error {
217226
return err
218227
}
219228

220-
allPods, err := o.KClient.Core().Pods(o.Namespace).List(metav1.ListOptions{})
229+
allPods, err := o.KubeClient.Core().Pods(o.Namespace).List(metav1.ListOptions{})
221230
if err != nil {
222231
return err
223232
}
224233

225-
allRCs, err := o.KClient.Core().ReplicationControllers(o.Namespace).List(metav1.ListOptions{})
234+
allRCs, err := o.KubeClient.Core().ReplicationControllers(o.Namespace).List(metav1.ListOptions{})
226235
if err != nil {
227236
return err
228237
}
@@ -246,7 +255,7 @@ func (o PruneImagesOptions) Run() error {
246255
return err
247256
}
248257

249-
limitRangesList, err := o.KClient.Core().LimitRanges(o.Namespace).List(metav1.ListOptions{})
258+
limitRangesList, err := o.KubeClient.Core().LimitRanges(o.Namespace).List(metav1.ListOptions{})
250259
if err != nil {
251260
return err
252261
}
@@ -261,6 +270,43 @@ func (o PruneImagesOptions) Run() error {
261270
limitRangesMap[limit.Namespace] = limits
262271
}
263272

273+
var (
274+
registryHost = o.RegistryUrlOverride
275+
registryClient *http.Client
276+
registryPinger prune.RegistryPinger
277+
)
278+
279+
if o.Confirm {
280+
if len(registryHost) == 0 {
281+
registryHost, err = prune.DetermineRegistryHost(allImages, allStreams)
282+
if err != nil {
283+
return fmt.Errorf("unable to determine registry: %v", err)
284+
}
285+
}
286+
287+
insecure := o.ForceInsecure
288+
if !insecure && len(o.CABundle) == 0 {
289+
insecure = o.ClientConfig.TLSClientConfig.Insecure || netutils.IsPrivateAddress(registryHost) ||
290+
strings.HasPrefix(registryHost, "http://")
291+
}
292+
293+
registryClient, err = getRegistryClient(o.ClientConfig, o.CABundle, insecure)
294+
if err != nil {
295+
return err
296+
}
297+
registryPinger = &prune.DefaultRegistryPinger{
298+
Client: registryClient,
299+
Insecure: insecure,
300+
}
301+
} else {
302+
registryPinger = &prune.DryRunRegistryPinger{}
303+
}
304+
305+
registryURL, err := registryPinger.Ping(registryHost)
306+
if err != nil {
307+
return fmt.Errorf("error communicating with registry %s: %v", registryHost, err)
308+
}
309+
264310
options := prune.PrunerOptions{
265311
KeepYoungerThan: o.KeepYoungerThan,
266312
KeepTagRevisions: o.KeepTagRevisions,
@@ -275,9 +321,8 @@ func (o PruneImagesOptions) Run() error {
275321
DCs: allDCs,
276322
LimitRanges: limitRangesMap,
277323
DryRun: o.Confirm == false,
278-
RegistryClient: o.RegistryClient,
279-
RegistryURL: o.RegistryUrlOverride,
280-
Insecure: o.Insecure,
324+
RegistryClient: registryClient,
325+
RegistryURL: registryURL,
281326
}
282327
if o.Namespace != metav1.NamespaceAll {
283328
options.Namespace = o.Namespace
@@ -316,7 +361,11 @@ type describingImageStreamDeleter struct {
316361

317362
var _ prune.ImageStreamDeleter = &describingImageStreamDeleter{}
318363

319-
func (p *describingImageStreamDeleter) DeleteImageStream(stream *imageapi.ImageStream, image *imageapi.Image, updatedTags []string) (*imageapi.ImageStream, error) {
364+
func (p *describingImageStreamDeleter) GetImageStream(stream *imageapi.ImageStream) (*imageapi.ImageStream, error) {
365+
return stream, nil
366+
}
367+
368+
func (p *describingImageStreamDeleter) UpdateImageStream(stream *imageapi.ImageStream, image *imageapi.Image, updatedTags []string) (*imageapi.ImageStream, error) {
320369
if !p.headerPrinted {
321370
p.headerPrinted = true
322371
fmt.Fprintln(p.w, "Deleting references from image streams to images ...")
@@ -329,7 +378,7 @@ func (p *describingImageStreamDeleter) DeleteImageStream(stream *imageapi.ImageS
329378
return stream, nil
330379
}
331380

332-
updatedStream, err := p.delegate.DeleteImageStream(stream, image, updatedTags)
381+
updatedStream, err := p.delegate.UpdateImageStream(stream, image, updatedTags)
333382
if err != nil {
334383
fmt.Fprintf(os.Stderr, "error updating image stream %s/%s to remove references to image %s: %v\n", stream.Namespace, stream.Name, image.Name, err)
335384
}
@@ -378,7 +427,7 @@ type describingLayerLinkDeleter struct {
378427

379428
var _ prune.LayerLinkDeleter = &describingLayerLinkDeleter{}
380429

381-
func (p *describingLayerLinkDeleter) DeleteLayerLink(registryClient *http.Client, registryURL, repo, name string) error {
430+
func (p *describingLayerLinkDeleter) DeleteLayerLink(registryClient *http.Client, registryURL *url.URL, repo, name string) error {
382431
if !p.headerPrinted {
383432
p.headerPrinted = true
384433
fmt.Fprintln(p.w, "\nDeleting registry repository layer links ...")
@@ -409,7 +458,7 @@ type describingBlobDeleter struct {
409458

410459
var _ prune.BlobDeleter = &describingBlobDeleter{}
411460

412-
func (p *describingBlobDeleter) DeleteBlob(registryClient *http.Client, registryURL, layer string) error {
461+
func (p *describingBlobDeleter) DeleteBlob(registryClient *http.Client, registryURL *url.URL, layer string) error {
413462
if !p.headerPrinted {
414463
p.headerPrinted = true
415464
fmt.Fprintln(p.w, "\nDeleting registry layer blobs ...")
@@ -441,7 +490,7 @@ type describingManifestDeleter struct {
441490

442491
var _ prune.ManifestDeleter = &describingManifestDeleter{}
443492

444-
func (p *describingManifestDeleter) DeleteManifest(registryClient *http.Client, registryURL, repo, manifest string) error {
493+
func (p *describingManifestDeleter) DeleteManifest(registryClient *http.Client, registryURL *url.URL, repo, manifest string) error {
445494
if !p.headerPrinted {
446495
p.headerPrinted = true
447496
fmt.Fprintln(p.w, "\nDeleting registry repository manifest data ...")
@@ -456,46 +505,48 @@ func (p *describingManifestDeleter) DeleteManifest(registryClient *http.Client,
456505

457506
err := p.delegate.DeleteManifest(registryClient, registryURL, repo, manifest)
458507
if err != nil {
459-
fmt.Fprintf(os.Stderr, "error deleting data for repository %s image manifest %s from the registry: %v\n", repo, manifest, err)
508+
fmt.Fprintf(os.Stderr, "error deleting manifest %s from repository %s: %v\n", manifest, repo, err)
460509
}
461510

462511
return err
463512
}
464513

465-
// getClients returns a Kube client, OpenShift client, and registry client. Note that
466-
// registryCABundle and registryInsecure=true are mutually exclusive. If registryInsecure=true is
467-
// specified, the ca bundle is ignored.
468-
func getClients(f *clientcmd.Factory, registryCABundle string, registryInsecure bool) (*client.Client, kclientset.Interface, *http.Client, error) {
514+
// getClients returns a OpenShift client and Kube client.
515+
func getClients(f *clientcmd.Factory) (*client.Client, kclientset.Interface, error) {
469516
clientConfig, err := f.ClientConfig()
470517
if err != nil {
471-
return nil, nil, nil, err
518+
return nil, nil, err
519+
}
520+
521+
if len(clientConfig.BearerToken) == 0 {
522+
return nil, nil, errNoToken
472523
}
473524

525+
osClient, kubeClient, err := f.Clients()
526+
if err != nil {
527+
return nil, nil, err
528+
}
529+
return osClient, kubeClient, err
530+
}
531+
532+
// getRegistryClient returns a registry client. Note that registryCABundle and registryInsecure=true are
533+
// mutually exclusive. If registryInsecure=true is specified, the ca bundle is ignored.
534+
func getRegistryClient(clientConfig *restclient.Config, registryCABundle string, registryInsecure bool) (*http.Client, error) {
474535
var (
475-
token string
476-
osClient *client.Client
477-
kClient kclientset.Interface
478-
registryClient *http.Client
536+
err error
537+
cadata []byte
538+
registryCABundleIncluded = false
539+
token = clientConfig.BearerToken
479540
)
480541

481-
switch {
482-
case len(clientConfig.BearerToken) > 0:
483-
osClient, kClient, err = f.Clients()
484-
if err != nil {
485-
return nil, nil, nil, err
486-
}
487-
token = clientConfig.BearerToken
488-
default:
489-
err = errors.New("you must use a client config with a token")
490-
return nil, nil, nil, err
542+
if len(token) == 0 {
543+
return nil, errNoToken
491544
}
492545

493-
cadata := []byte{}
494-
registryCABundleIncluded := false
495546
if len(registryCABundle) > 0 {
496547
cadata, err = ioutil.ReadFile(registryCABundle)
497548
if err != nil {
498-
return nil, nil, nil, fmt.Errorf("failed to read registry ca bundle: %v", err)
549+
return nil, fmt.Errorf("failed to read registry ca bundle: %v", err)
499550
}
500551
}
501552

@@ -531,7 +582,7 @@ func getClients(f *clientcmd.Factory, registryCABundle string, registryInsecure
531582

532583
tlsConfig, err := restclient.TLSConfigFor(&registryClientConfig)
533584
if err != nil {
534-
return nil, nil, nil, err
585+
return nil, err
535586
}
536587

537588
// Add the CA bundle to the client config's CA roots if provided and we haven't done that already.
@@ -549,12 +600,10 @@ func getClients(f *clientcmd.Factory, registryCABundle string, registryInsecure
549600

550601
wrappedTransport, err := restclient.HTTPWrappersForConfig(&registryClientConfig, transport)
551602
if err != nil {
552-
return nil, nil, nil, err
603+
return nil, err
553604
}
554605

555-
registryClient = &http.Client{
606+
return &http.Client{
556607
Transport: wrappedTransport,
557-
}
558-
559-
return osClient, kClient, registryClient, nil
608+
}, nil
560609
}

pkg/cmd/admin/prune/images_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ func TestImagePruneNamespaced(t *testing.T) {
1515
opts := &PruneImagesOptions{
1616
Namespace: "foo",
1717

18-
OSClient: osFake,
19-
KClient: kFake,
20-
Out: ioutil.Discard,
18+
OSClient: osFake,
19+
KubeClient: kFake,
20+
Out: ioutil.Discard,
2121
}
2222

2323
if err := opts.Run(); err != nil {

pkg/image/apis/image/helper.go

Lines changed: 42 additions & 0 deletions
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+
}

0 commit comments

Comments
 (0)