Browse code

Merge pull request #11973 from smarterclayton/mirror_pullthrough

Merged by openshift-bot

OpenShift Bot authored on 2016/12/09 17:36:39
Showing 18 changed files
... ...
@@ -4108,6 +4108,8 @@ _oadm_prune_images()
4108 4108
     flags_with_completion=()
4109 4109
     flags_completion=()
4110 4110
 
4111
+    flags+=("--all")
4112
+    local_nonpersistent_flags+=("--all")
4111 4113
     flags+=("--certificate-authority=")
4112 4114
     local_nonpersistent_flags+=("--certificate-authority=")
4113 4115
     flags+=("--confirm")
... ...
@@ -4161,6 +4161,8 @@ _oc_adm_prune_images()
4161 4161
     flags_with_completion=()
4162 4162
     flags_completion=()
4163 4163
 
4164
+    flags+=("--all")
4165
+    local_nonpersistent_flags+=("--all")
4164 4166
     flags+=("--certificate-authority=")
4165 4167
     local_nonpersistent_flags+=("--certificate-authority=")
4166 4168
     flags+=("--confirm")
... ...
@@ -4108,6 +4108,8 @@ _openshift_admin_prune_images()
4108 4108
     flags_with_completion=()
4109 4109
     flags_completion=()
4110 4110
 
4111
+    flags+=("--all")
4112
+    local_nonpersistent_flags+=("--all")
4111 4113
     flags+=("--certificate-authority=")
4112 4114
     local_nonpersistent_flags+=("--certificate-authority=")
4113 4115
     flags+=("--confirm")
... ...
@@ -8723,6 +8725,8 @@ _openshift_cli_adm_prune_images()
8723 8723
     flags_with_completion=()
8724 8724
     flags_completion=()
8725 8725
 
8726
+    flags+=("--all")
8727
+    local_nonpersistent_flags+=("--all")
8726 8728
     flags+=("--certificate-authority=")
8727 8729
     local_nonpersistent_flags+=("--certificate-authority=")
8728 8730
     flags+=("--confirm")
... ...
@@ -4269,6 +4269,8 @@ _oadm_prune_images()
4269 4269
     flags_with_completion=()
4270 4270
     flags_completion=()
4271 4271
 
4272
+    flags+=("--all")
4273
+    local_nonpersistent_flags+=("--all")
4272 4274
     flags+=("--certificate-authority=")
4273 4275
     local_nonpersistent_flags+=("--certificate-authority=")
4274 4276
     flags+=("--confirm")
... ...
@@ -4322,6 +4322,8 @@ _oc_adm_prune_images()
4322 4322
     flags_with_completion=()
4323 4323
     flags_completion=()
4324 4324
 
4325
+    flags+=("--all")
4326
+    local_nonpersistent_flags+=("--all")
4325 4327
     flags+=("--certificate-authority=")
4326 4328
     local_nonpersistent_flags+=("--certificate-authority=")
4327 4329
     flags+=("--confirm")
... ...
@@ -4269,6 +4269,8 @@ _openshift_admin_prune_images()
4269 4269
     flags_with_completion=()
4270 4270
     flags_completion=()
4271 4271
 
4272
+    flags+=("--all")
4273
+    local_nonpersistent_flags+=("--all")
4272 4274
     flags+=("--certificate-authority=")
4273 4275
     local_nonpersistent_flags+=("--certificate-authority=")
4274 4276
     flags+=("--confirm")
... ...
@@ -8884,6 +8886,8 @@ _openshift_cli_adm_prune_images()
8884 8884
     flags_with_completion=()
8885 8885
     flags_completion=()
8886 8886
 
8887
+    flags+=("--all")
8888
+    local_nonpersistent_flags+=("--all")
8887 8889
     flags+=("--certificate-authority=")
8888 8890
     local_nonpersistent_flags+=("--certificate-authority=")
8889 8891
     flags+=("--confirm")
... ...
@@ -649,7 +649,7 @@ Remove unreferenced images
649 649
   oadm prune images --keep-tag-revisions=3 --keep-younger-than=60m --confirm
650 650
   
651 651
   # See, what the prune command would delete if we're interested in removing images
652
-  # exceeding currently set LimitRanges ('openshift.io/Image')
652
+  # exceeding currently set limit ranges ('openshift.io/Image')
653 653
   oadm prune images --prune-over-size-limit
654 654
   
655 655
   # To actually perform the prune operation, the confirm flag must be appended
... ...
@@ -649,7 +649,7 @@ Remove unreferenced images
649 649
   oc adm prune images --keep-tag-revisions=3 --keep-younger-than=60m --confirm
650 650
   
651 651
   # See, what the prune command would delete if we're interested in removing images
652
-  # exceeding currently set LimitRanges ('openshift.io/Image')
652
+  # exceeding currently set limit ranges ('openshift.io/Image')
653 653
   oc adm prune images --prune-over-size-limit
654 654
   
655 655
   # To actually perform the prune operation, the confirm flag must be appended
... ...
@@ -13,7 +13,10 @@ oadm prune images \- Remove unreferenced images
13 13
 
14 14
 .SH DESCRIPTION
15 15
 .PP
16
-Prune images no longer needed due to age and/or status
16
+Remove image stream tags, images, and image layers by age or usage
17
+
18
+.PP
19
+This command removes historical image stream tags, unused images, and unreferenced image layers from the integrated registry. It prefers images that have been directly pushed to the registry, but you may specify \-\-all to include images that were imported (if registry mirroring is enabled).
17 20
 
18 21
 .PP
19 22
 By default, the prune operation performs a dry run making no changes to internal registry. A \-\-confirm flag is needed for changes to be effective.
... ...
@@ -24,6 +27,10 @@ Only a user with a cluster role system:image\-pruner or higher who is logged\-in
24 24
 
25 25
 .SH OPTIONS
26 26
 .PP
27
+\fB\-\-all\fP=false
28
+    Include images that were not pushed to the registry but have been mirrored by pullthrough. Requires \-\-registry\-url
29
+
30
+.PP
27 31
 \fB\-\-certificate\-authority\fP=""
28 32
     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.
29 33
 
... ...
@@ -127,7 +134,7 @@ Only a user with a cluster role system:image\-pruner or higher who is logged\-in
127 127
   oadm prune images \-\-keep\-tag\-revisions=3 \-\-keep\-younger\-than=60m \-\-confirm
128 128
   
129 129
   # See, what the prune command would delete if we're interested in removing images
130
-  # exceeding currently set LimitRanges ('openshift.io/Image')
130
+  # exceeding currently set limit ranges ('openshift.io/Image')
131 131
   oadm prune images \-\-prune\-over\-size\-limit
132 132
   
133 133
   # To actually perform the prune operation, the confirm flag must be appended
... ...
@@ -13,7 +13,10 @@ oc adm prune images \- Remove unreferenced images
13 13
 
14 14
 .SH DESCRIPTION
15 15
 .PP
16
-Prune images no longer needed due to age and/or status
16
+Remove image stream tags, images, and image layers by age or usage
17
+
18
+.PP
19
+This command removes historical image stream tags, unused images, and unreferenced image layers from the integrated registry. It prefers images that have been directly pushed to the registry, but you may specify \-\-all to include images that were imported (if registry mirroring is enabled).
17 20
 
18 21
 .PP
19 22
 By default, the prune operation performs a dry run making no changes to internal registry. A \-\-confirm flag is needed for changes to be effective.
... ...
@@ -24,6 +27,10 @@ Only a user with a cluster role system:image\-pruner or higher who is logged\-in
24 24
 
25 25
 .SH OPTIONS
26 26
 .PP
27
+\fB\-\-all\fP=false
28
+    Include images that were not pushed to the registry but have been mirrored by pullthrough. Requires \-\-registry\-url
29
+
30
+.PP
27 31
 \fB\-\-certificate\-authority\fP=""
28 32
     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.
29 33
 
... ...
@@ -127,7 +134,7 @@ Only a user with a cluster role system:image\-pruner or higher who is logged\-in
127 127
   oc adm prune images \-\-keep\-tag\-revisions=3 \-\-keep\-younger\-than=60m \-\-confirm
128 128
   
129 129
   # See, what the prune command would delete if we're interested in removing images
130
-  # exceeding currently set LimitRanges ('openshift.io/Image')
130
+  # exceeding currently set limit ranges ('openshift.io/Image')
131 131
   oc adm prune images \-\-prune\-over\-size\-limit
132 132
   
133 133
   # To actually perform the prune operation, the confirm flag must be appended
... ...
@@ -13,7 +13,10 @@ openshift admin prune images \- Remove unreferenced images
13 13
 
14 14
 .SH DESCRIPTION
15 15
 .PP
16
-Prune images no longer needed due to age and/or status
16
+Remove image stream tags, images, and image layers by age or usage
17
+
18
+.PP
19
+This command removes historical image stream tags, unused images, and unreferenced image layers from the integrated registry. It prefers images that have been directly pushed to the registry, but you may specify \-\-all to include images that were imported (if registry mirroring is enabled).
17 20
 
18 21
 .PP
19 22
 By default, the prune operation performs a dry run making no changes to internal registry. A \-\-confirm flag is needed for changes to be effective.
... ...
@@ -24,6 +27,10 @@ Only a user with a cluster role system:image\-pruner or higher who is logged\-in
24 24
 
25 25
 .SH OPTIONS
26 26
 .PP
27
+\fB\-\-all\fP=false
28
+    Include images that were not pushed to the registry but have been mirrored by pullthrough. Requires \-\-registry\-url
29
+
30
+.PP
27 31
 \fB\-\-certificate\-authority\fP=""
28 32
     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.
29 33
 
... ...
@@ -127,7 +134,7 @@ Only a user with a cluster role system:image\-pruner or higher who is logged\-in
127 127
   openshift admin prune images \-\-keep\-tag\-revisions=3 \-\-keep\-younger\-than=60m \-\-confirm
128 128
   
129 129
   # See, what the prune command would delete if we're interested in removing images
130
-  # exceeding currently set LimitRanges ('openshift.io/Image')
130
+  # exceeding currently set limit ranges ('openshift.io/Image')
131 131
   openshift admin prune images \-\-prune\-over\-size\-limit
132 132
   
133 133
   # To actually perform the prune operation, the confirm flag must be appended
... ...
@@ -13,7 +13,10 @@ openshift cli adm prune images \- Remove unreferenced images
13 13
 
14 14
 .SH DESCRIPTION
15 15
 .PP
16
-Prune images no longer needed due to age and/or status
16
+Remove image stream tags, images, and image layers by age or usage
17
+
18
+.PP
19
+This command removes historical image stream tags, unused images, and unreferenced image layers from the integrated registry. It prefers images that have been directly pushed to the registry, but you may specify \-\-all to include images that were imported (if registry mirroring is enabled).
17 20
 
18 21
 .PP
19 22
 By default, the prune operation performs a dry run making no changes to internal registry. A \-\-confirm flag is needed for changes to be effective.
... ...
@@ -24,6 +27,10 @@ Only a user with a cluster role system:image\-pruner or higher who is logged\-in
24 24
 
25 25
 .SH OPTIONS
26 26
 .PP
27
+\fB\-\-all\fP=false
28
+    Include images that were not pushed to the registry but have been mirrored by pullthrough. Requires \-\-registry\-url
29
+
30
+.PP
27 31
 \fB\-\-certificate\-authority\fP=""
28 32
     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.
29 33
 
... ...
@@ -127,7 +134,7 @@ Only a user with a cluster role system:image\-pruner or higher who is logged\-in
127 127
   openshift cli adm prune images \-\-keep\-tag\-revisions=3 \-\-keep\-younger\-than=60m \-\-confirm
128 128
   
129 129
   # See, what the prune command would delete if we're interested in removing images
130
-  # exceeding currently set LimitRanges ('openshift.io/Image')
130
+  # exceeding currently set limit ranges ('openshift.io/Image')
131 131
   openshift cli adm prune images \-\-prune\-over\-size\-limit
132 132
   
133 133
   # To actually perform the prune operation, the confirm flag must be appended
... ...
@@ -13,7 +13,7 @@ storage:
13 13
 auth:
14 14
   openshift:
15 15
     realm: openshift
16
-    
16
+
17 17
     # tokenrealm is a base URL to use for the token-granting registry endpoint.
18 18
     # If unspecified, the scheme and host for the token redirect are determined from the incoming request.
19 19
     # If specified, a scheme and host must be chosen that all registry clients can resolve and access:
... ...
@@ -27,6 +27,7 @@ middleware:
27 27
       options:
28 28
         acceptschema2: false
29 29
         pullthrough: true
30
+        mirrorpullthrough: true
30 31
         enforcequota: false
31 32
         projectcachettl: 1m
32 33
         blobrepositorycachettl: 10m
... ...
@@ -34,7 +34,12 @@ const PruneImagesRecommendedName = "images"
34 34
 
35 35
 var (
36 36
 	imagesLongDesc = templates.LongDesc(`
37
-		Prune images no longer needed due to age and/or status
37
+		Remove image stream tags, images, and image layers by age or usage
38
+
39
+		This command removes historical image stream tags, unused images, and unreferenced image
40
+		layers from the integrated registry. It prefers images that have been directly pushed to
41
+		the registry, but you may specify --all to include images that were imported (if registry
42
+		mirroring is enabled).
38 43
 
39 44
 		By default, the prune operation performs a dry run making no changes to internal registry. A
40 45
 		--confirm flag is needed for changes to be effective.
... ...
@@ -51,7 +56,7 @@ var (
51 51
 	  %[1]s %[2]s --keep-tag-revisions=3 --keep-younger-than=60m --confirm
52 52
 
53 53
 	  # See, what the prune command would delete if we're interested in removing images
54
-	  # exceeding currently set LimitRanges ('openshift.io/Image')
54
+	  # exceeding currently set limit ranges ('openshift.io/Image')
55 55
 	  %[1]s %[2]s --prune-over-size-limit
56 56
 
57 57
 	  # To actually perform the prune operation, the confirm flag must be appended
... ...
@@ -70,6 +75,7 @@ type PruneImagesOptions struct {
70 70
 	KeepYoungerThan     *time.Duration
71 71
 	KeepTagRevisions    *int
72 72
 	PruneOverSizeLimit  *bool
73
+	AllImages           *bool
73 74
 	CABundle            string
74 75
 	RegistryUrlOverride string
75 76
 	Namespace           string
... ...
@@ -82,11 +88,13 @@ type PruneImagesOptions struct {
82 82
 
83 83
 // NewCmdPruneImages implements the OpenShift cli prune images command.
84 84
 func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Writer) *cobra.Command {
85
+	allImages := false
85 86
 	opts := &PruneImagesOptions{
86 87
 		Confirm:            false,
87 88
 		KeepYoungerThan:    &defaultKeepYoungerThan,
88 89
 		KeepTagRevisions:   &defaultKeepTagRevisions,
89 90
 		PruneOverSizeLimit: &defaultPruneImageOverSizeLimit,
91
+		AllImages:          &allImages,
90 92
 	}
91 93
 
92 94
 	cmd := &cobra.Command{
... ...
@@ -104,6 +112,7 @@ func NewCmdPruneImages(f *clientcmd.Factory, parentName, name string, out io.Wri
104 104
 	}
105 105
 
106 106
 	cmd.Flags().BoolVar(&opts.Confirm, "confirm", opts.Confirm, "Specify that image pruning should proceed. Defaults to false, displaying what would be deleted but not actually deleting anything.")
107
+	cmd.Flags().BoolVar(opts.AllImages, "all", *opts.AllImages, "Include images that were not pushed to the registry but have been mirrored by pullthrough. Requires --registry-url")
107 108
 	cmd.Flags().DurationVar(opts.KeepYoungerThan, "keep-younger-than", *opts.KeepYoungerThan, "Specify the minimum age of an image for it to be considered a candidate for pruning.")
108 109
 	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.")
109 110
 	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.")
... ...
@@ -130,6 +139,11 @@ func (o *PruneImagesOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command,
130 130
 			o.KeepTagRevisions = nil
131 131
 		}
132 132
 	}
133
+	if *o.AllImages {
134
+		if len(o.RegistryUrlOverride) == 0 {
135
+			return kcmdutil.UsageError(cmd, "--registry-url must be specified when --all is true")
136
+		}
137
+	}
133 138
 	o.Namespace = kapi.NamespaceAll
134 139
 	if cmd.Flags().Lookup("namespace").Changed {
135 140
 		var err error
... ...
@@ -228,6 +242,7 @@ func (o PruneImagesOptions) Run() error {
228 228
 		KeepYoungerThan:    o.KeepYoungerThan,
229 229
 		KeepTagRevisions:   o.KeepTagRevisions,
230 230
 		PruneOverSizeLimit: o.PruneOverSizeLimit,
231
+		AllImages:          o.AllImages,
231 232
 		Images:             allImages,
232 233
 		Streams:            allStreams,
233 234
 		Pods:               allPods,
... ...
@@ -1,8 +1,9 @@
1 1
 package server
2 2
 
3 3
 import (
4
+	"io"
4 5
 	"net/http"
5
-	"strconv"
6
+	"sync"
6 7
 	"time"
7 8
 
8 9
 	"github.com/docker/distribution"
... ...
@@ -23,6 +24,7 @@ type pullthroughBlobStore struct {
23 23
 	repo                       *repository
24 24
 	digestToStore              map[string]distribution.BlobStore
25 25
 	pullFromInsecureRegistries bool
26
+	mirror                     bool
26 27
 }
27 28
 
28 29
 var _ distribution.BlobStore = &pullthroughBlobStore{}
... ...
@@ -116,30 +118,36 @@ func (r *pullthroughBlobStore) proxyStat(ctx context.Context, retriever importer
116 116
 }
117 117
 
118 118
 // ServeBlob attempts to serve the requested digest onto w, using a remote proxy store if necessary.
119
-func (r *pullthroughBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, req *http.Request, dgst digest.Digest) error {
120
-	store, ok := r.digestToStore[dgst.String()]
119
+func (pbs *pullthroughBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, req *http.Request, dgst digest.Digest) error {
120
+	store, ok := pbs.digestToStore[dgst.String()]
121 121
 	if !ok {
122
-		return r.BlobStore.ServeBlob(ctx, w, req, dgst)
122
+		return pbs.BlobStore.ServeBlob(ctx, w, req, dgst)
123 123
 	}
124 124
 
125
-	desc, err := store.Stat(ctx, dgst)
126
-	if err != nil {
127
-		context.GetLogger(ctx).Errorf("failed to stat digest %q: %v", dgst.String(), err)
128
-		return err
129
-	}
125
+	// store the content locally if requested, but ensure only one instance at a time
126
+	// is storing to avoid excessive local writes
127
+	if pbs.mirror {
128
+		mu.Lock()
129
+		if _, ok = inflight[dgst]; ok {
130
+			mu.Unlock()
131
+			context.GetLogger(ctx).Infof("Serving %q while mirroring in background", dgst)
132
+			_, err := pbs.copyContent(store, ctx, dgst, w, req)
133
+			return err
134
+		}
135
+		inflight[dgst] = struct{}{}
136
+		mu.Unlock()
130 137
 
131
-	remoteReader, err := store.Open(ctx, dgst)
132
-	if err != nil {
133
-		context.GetLogger(ctx).Errorf("failure to open remote store for digest %q: %v", dgst.String(), err)
134
-		return err
138
+		go func(dgst digest.Digest) {
139
+			context.GetLogger(ctx).Infof("Start background mirroring of %q", dgst)
140
+			if err := pbs.storeLocal(store, ctx, dgst); err != nil {
141
+				context.GetLogger(ctx).Errorf("Error committing to storage: %s", err.Error())
142
+			}
143
+			context.GetLogger(ctx).Infof("Completed mirroring of %q", dgst)
144
+		}(dgst)
135 145
 	}
136
-	defer remoteReader.Close()
137 146
 
138
-	setResponseHeaders(w, desc.Size, desc.MediaType, dgst)
139
-
140
-	context.GetLogger(ctx).Infof("serving blob %s of type %s %d bytes long", dgst.String(), desc.MediaType, desc.Size)
141
-	http.ServeContent(w, req, desc.Digest.String(), time.Time{}, remoteReader)
142
-	return nil
147
+	_, err := pbs.copyContent(store, ctx, dgst, w, req)
148
+	return err
143 149
 }
144 150
 
145 151
 // Get attempts to fetch the requested blob by digest using a remote proxy store if necessary.
... ...
@@ -239,8 +247,72 @@ func identifyCandidateRepositories(is *imageapi.ImageStream, localRegistry strin
239 239
 
240 240
 // setResponseHeaders sets the appropriate content serving headers
241 241
 func setResponseHeaders(w http.ResponseWriter, length int64, mediaType string, digest digest.Digest) {
242
-	w.Header().Set("Content-Length", strconv.FormatInt(length, 10))
243 242
 	w.Header().Set("Content-Type", mediaType)
244 243
 	w.Header().Set("Docker-Content-Digest", digest.String())
245 244
 	w.Header().Set("Etag", digest.String())
246 245
 }
246
+
247
+// inflight tracks currently downloading blobs
248
+var inflight = make(map[digest.Digest]struct{})
249
+
250
+// mu protects inflight
251
+var mu sync.Mutex
252
+
253
+// copyContent attempts to load and serve the provided blob. If req != nil and writer is an instance of http.ResponseWriter,
254
+// response headers will be set and range requests honored.
255
+func (pbs *pullthroughBlobStore) copyContent(store distribution.BlobStore, ctx context.Context, dgst digest.Digest, writer io.Writer, req *http.Request) (distribution.Descriptor, error) {
256
+	desc, err := store.Stat(ctx, dgst)
257
+	if err != nil {
258
+		return distribution.Descriptor{}, err
259
+	}
260
+
261
+	remoteReader, err := store.Open(ctx, dgst)
262
+	if err != nil {
263
+		return distribution.Descriptor{}, err
264
+	}
265
+
266
+	rw, ok := writer.(http.ResponseWriter)
267
+	if ok {
268
+		setResponseHeaders(rw, desc.Size, desc.MediaType, dgst)
269
+		// serve range requests
270
+		if req != nil {
271
+			http.ServeContent(rw, req, desc.Digest.String(), time.Time{}, remoteReader)
272
+			return desc, nil
273
+		}
274
+	}
275
+
276
+	if _, err = io.CopyN(writer, remoteReader, desc.Size); err != nil {
277
+		return distribution.Descriptor{}, err
278
+	}
279
+	return desc, nil
280
+}
281
+
282
+// storeLocal retrieves the named blob from the provided store and writes it into the local store.
283
+func (pbs *pullthroughBlobStore) storeLocal(store distribution.BlobStore, ctx context.Context, dgst digest.Digest) error {
284
+	defer func() {
285
+		mu.Lock()
286
+		delete(inflight, dgst)
287
+		mu.Unlock()
288
+	}()
289
+
290
+	var desc distribution.Descriptor
291
+	var err error
292
+	var bw distribution.BlobWriter
293
+
294
+	bw, err = pbs.BlobStore.Create(ctx)
295
+	if err != nil {
296
+		return err
297
+	}
298
+
299
+	desc, err = pbs.copyContent(store, ctx, dgst, bw, nil)
300
+	if err != nil {
301
+		return err
302
+	}
303
+
304
+	_, err = bw.Commit(ctx, desc)
305
+	if err != nil {
306
+		return err
307
+	}
308
+
309
+	return nil
310
+}
... ...
@@ -55,6 +55,12 @@ const (
55 55
 	// leaking a blob that is no longer tagged in given repository.
56 56
 	BlobRepositoryCacheTTLEnvVar = "REGISTRY_MIDDLEWARE_REPOSITORY_OPENSHIFT_BLOBREPOSITORYCACHETTL"
57 57
 
58
+	// Pullthrough is a boolean environment variable that controls whether pullthrough is enabled.
59
+	PullthroughEnvVar = "REGISTRY_MIDDLEWARE_REPOSITORY_OPENSHIFT_PULLTHROUGH"
60
+
61
+	// MirrorPullthrough is a boolean environment variable that controls mirroring of blobs on pullthrough.
62
+	MirrorPullthroughEnvVar = "REGISTRY_MIDDLEWARE_REPOSITORY_OPENSHIFT_MIRRORPULLTHROUGH"
63
+
58 64
 	// Default values
59 65
 
60 66
 	defaultDigestToRepositoryCacheSize = 2048
... ...
@@ -136,6 +142,8 @@ type repository struct {
136 136
 	// if true, the repository will check remote references in the image stream to support pulling "through"
137 137
 	// from a remote repository
138 138
 	pullthrough bool
139
+	// mirrorPullthrough will mirror remote blobs into the local repository if set
140
+	mirrorPullthrough bool
139 141
 	// acceptschema2 allows to refuse the manifest schema version 2
140 142
 	acceptschema2 bool
141 143
 	// blobrepositorycachettl is an eviction timeout for <blob belongs to repository> entries of cachedLayers
... ...
@@ -170,7 +178,11 @@ func newRepositoryWithClient(
170 170
 	if err != nil {
171 171
 		context.GetLogger(ctx).Error(err)
172 172
 	}
173
-	pullthrough, err := getBoolOption("", "pullthrough", false, options)
173
+	pullthrough, err := getBoolOption(PullthroughEnvVar, "pullthrough", false, options)
174
+	if err != nil {
175
+		context.GetLogger(ctx).Error(err)
176
+	}
177
+	mirrorPullthrough, err := getBoolOption(MirrorPullthroughEnvVar, "mirrorpullthrough", true, options)
174 178
 	if err != nil {
175 179
 		context.GetLogger(ctx).Error(err)
176 180
 	}
... ...
@@ -193,6 +205,7 @@ func newRepositoryWithClient(
193 193
 		acceptschema2:          acceptschema2,
194 194
 		blobrepositorycachettl: blobrepositorycachettl,
195 195
 		pullthrough:            pullthrough,
196
+		mirrorPullthrough:      mirrorPullthrough,
196 197
 		cachedLayers:           cachedLayers,
197 198
 	}, nil
198 199
 }
... ...
@@ -228,6 +241,7 @@ func (r *repository) Blobs(ctx context.Context) distribution.BlobStore {
228 228
 
229 229
 			repo:          &repo,
230 230
 			digestToStore: make(map[string]distribution.BlobStore),
231
+			mirror:        r.mirrorPullthrough,
231 232
 		}
232 233
 	}
233 234
 
... ...
@@ -55,6 +55,7 @@ type pruneAlgorithm struct {
55 55
 	keepTagRevisions   int
56 56
 	pruneOverSizeLimit bool
57 57
 	namespace          string
58
+	allImages          bool
58 59
 }
59 60
 
60 61
 // ImageDeleter knows how to remove images from OpenShift.
... ...
@@ -103,6 +104,9 @@ type PrunerOptions struct {
103 103
 	// PruneOverSizeLimit indicates that images exceeding defined limits (openshift.io/Image)
104 104
 	// will be considered as candidates for pruning.
105 105
 	PruneOverSizeLimit *bool
106
+	// AllImages considers all images for pruning, not just those pushed directly to the registry.
107
+	// Requires RegistryURL be set.
108
+	AllImages *bool
106 109
 	// Namespace to be pruned, if specified it should never remove Images.
107 110
 	Namespace string
108 111
 	// Images is the entire list of images in OpenShift. An image must be in this
... ...
@@ -224,9 +228,10 @@ func (*dryRunRegistryPinger) ping(registry string) error {
224 224
 // cluster; otherwise, the pruning algorithm might result in incorrect
225 225
 // calculations and premature pruning.
226 226
 //
227
-// The ImageDeleter performs the following logic: remove any image containing the
228
-// annotation openshift.io/image.managed=true that was created at least *n*
229
-// minutes ago and is *not* currently referenced by:
227
+// The ImageDeleter performs the following logic:
228
+//
229
+// remove any image that was created at least *n* minutes ago and is *not*
230
+// currently referenced by:
230 231
 //
231 232
 // - any pod created less than *n* minutes ago
232 233
 // - any image stream created less than *n* minutes ago
... ...
@@ -238,6 +243,9 @@ func (*dryRunRegistryPinger) ping(registry string) error {
238 238
 // - any builds
239 239
 // - the n most recent tag revisions in an image stream's status.tags
240 240
 //
241
+// including only images with the annotation openshift.io/image.managed=true
242
+// unless allImages is true.
243
+//
241 244
 // When removing an image, remove all references to the image from all
242 245
 // ImageStreams having a reference to the image in `status.tags`.
243 246
 //
... ...
@@ -252,8 +260,8 @@ func NewPruner(options PrunerOptions) Pruner {
252 252
 	if options.PruneOverSizeLimit != nil {
253 253
 		pruneOverSizeLimit = fmt.Sprintf("%v", *options.PruneOverSizeLimit)
254 254
 	}
255
-	glog.V(1).Infof("Creating image pruner with keepYoungerThan=%v, keepTagRevisions=%s, pruneOverSizeLimit=%s",
256
-		options.KeepYoungerThan, keepTagRevisions, pruneOverSizeLimit)
255
+	glog.V(1).Infof("Creating image pruner with keepYoungerThan=%v, keepTagRevisions=%s, pruneOverSizeLimit=%s, allImages=%t",
256
+		options.KeepYoungerThan, keepTagRevisions, pruneOverSizeLimit, options.AllImages)
257 257
 
258 258
 	algorithm := pruneAlgorithm{}
259 259
 	if options.KeepYoungerThan != nil {
... ...
@@ -265,6 +273,9 @@ func NewPruner(options PrunerOptions) Pruner {
265 265
 	if options.PruneOverSizeLimit != nil {
266 266
 		algorithm.pruneOverSizeLimit = *options.PruneOverSizeLimit
267 267
 	}
268
+	if options.AllImages != nil {
269
+		algorithm.allImages = *options.AllImages
270
+	}
268 271
 	algorithm.namespace = options.Namespace
269 272
 
270 273
 	g := graph.New()
... ...
@@ -302,13 +313,11 @@ func addImagesToGraph(g graph.Graph, images *imageapi.ImageList, algorithm prune
302 302
 
303 303
 		glog.V(4).Infof("Examining image %q", image.Name)
304 304
 
305
-		if image.Annotations == nil {
306
-			glog.V(4).Infof("Image %q with DockerImageReference %q belongs to an external registry - skipping", image.Name, image.DockerImageReference)
307
-			continue
308
-		}
309
-		if value, ok := image.Annotations[imageapi.ManagedByOpenShiftAnnotation]; !ok || value != "true" {
310
-			glog.V(4).Infof("Image %q with DockerImageReference %q belongs to an external registry - skipping", image.Name, image.DockerImageReference)
311
-			continue
305
+		if !algorithm.allImages {
306
+			if image.Annotations[imageapi.ManagedByOpenShiftAnnotation] != "true" {
307
+				glog.V(4).Infof("Image %q with DockerImageReference %q belongs to an external registry - skipping", image.Name, image.DockerImageReference)
308
+				continue
309
+			}
312 310
 		}
313 311
 
314 312
 		age := unversioned.Now().Sub(image.CreationTimestamp.Time)
... ...
@@ -105,6 +105,7 @@ os::cmd::try_until_success "curl --max-time 2 --fail --silent 'http://${DOCKER_R
105 105
 os::cmd::expect_success "curl -f http://${DOCKER_REGISTRY}/healthz"
106 106
 
107 107
 os::cmd::expect_success "dig @${API_HOST} docker-registry.default.local. A"
108
+registry_pod="$(oc get pod -n default -l deploymentconfig=docker-registry --template='{{(index .items 0).metadata.name}}')"
108 109
 
109 110
 # Client setup (log in as e2e-user and set 'test' as the default project)
110 111
 # This is required to be able to push to the registry!
... ...
@@ -147,6 +148,11 @@ os::log::info "Ruby's testing blob digest: $rubyimageblob"
147 147
 os::log::info "Docker pullthrough"
148 148
 os::cmd::expect_success "oc import-image --confirm --from=mysql:latest mysql:pullthrough"
149 149
 os::cmd::expect_success "docker pull ${DOCKER_REGISTRY}/cache/mysql:pullthrough"
150
+mysqlblob="$(oc get istag -o go-template='{{range .image.dockerImageLayers}}{{if gt .size 4096.}}{{.name}},{{end}}{{end}}' "mysql:pullthrough" | cut -d , -f 1)"
151
+# directly hit the image to trigger mirroring in case the layer already exists on disk
152
+os::cmd::expect_success "curl -H 'Authorization: bearer $(oc whoami -t)' 'http://${DOCKER_REGISTRY}/v2/cache/mysql/blobs/${mysqlblob}' 1>/dev/null"
153
+# verify the blob exists on disk in the registry due to mirroring under .../blobs/sha256/<2 char prefix>/<sha value>
154
+os::cmd::try_until_success "oc exec --context='${CLUSTER_ADMIN_CONTEXT}' -n default -p '${registry_pod}' du /registry | tee '${LOG_DIR}/registry-images.txt' | grep '${mysqlblob:7:100}' | grep blobs"
150 155
 
151 156
 os::log::info "Docker registry start with GCS"
152 157
 os::cmd::expect_failure_and_text "docker run -e REGISTRY_STORAGE=\"gcs: {}\" openshift/origin-docker-registry:${TAG}" "No bucket parameter provided"
... ...
@@ -522,23 +528,35 @@ os::cmd::expect_success "docker tag gcr.io/google_containers/pause ${DOCKER_REGI
522 522
 os::cmd::expect_success "docker push ${DOCKER_REGISTRY}/cache/prune"
523 523
 
524 524
 # record the storage before pruning
525
-registry_pod=$(oc get pod -l deploymentconfig=docker-registry --template='{{(index .items 0).metadata.name}}')
525
+registry_pod=$(oc get pod -n default -l deploymentconfig=docker-registry --template='{{(index .items 0).metadata.name}}')
526 526
 os::cmd::expect_success "oc exec -p ${registry_pod} du /registry > '${LOG_DIR}/prune-images.before.txt'"
527 527
 
528 528
 # set up pruner user
529
-os::cmd::expect_success 'oadm policy add-cluster-role-to-user system:image-pruner e2e-pruner'
530
-os::cmd::try_until_text 'oadm policy who-can list images' 'e2e-pruner'
531
-os::cmd::expect_success 'oc login -u e2e-pruner -p pass'
529
+os::cmd::expect_success 'oadm policy add-cluster-role-to-user system:image-pruner system:serviceaccount:cache:builder'
530
+os::cmd::try_until_text 'oadm policy who-can list images' 'system:serviceaccount:cache:builder'
532 531
 
533 532
 # run image pruning
534
-os::cmd::expect_success_and_not_text "oadm prune images --keep-younger-than=0 --keep-tag-revisions=1 --confirm" 'error'
533
+os::cmd::expect_success_and_not_text "oadm prune images --token='$(oc sa get-token builder -n cache)' --keep-younger-than=0 --keep-tag-revisions=1 --confirm" 'error'
535 534
 
536
-os::cmd::expect_success "oc project ${CLUSTER_ADMIN_CONTEXT}"
537 535
 # record the storage after pruning
538 536
 os::cmd::expect_success "oc exec -p ${registry_pod} du /registry > '${LOG_DIR}/prune-images.after.txt'"
539 537
 
540 538
 # make sure there were changes to the registry's storage
541 539
 os::cmd::expect_code "diff ${LOG_DIR}/prune-images.before.txt ${LOG_DIR}/prune-images.after.txt" 1
540
+
541
+# prune a mirror, external image that is no longer referenced
542
+os::cmd::expect_success "oc import-image nginx --confirm -n cache"
543
+nginxblob="$(oc get istag -o go-template='{{range .image.dockerImageLayers}}{{if gt .size 4096.}}{{.name}},{{end}}{{end}}' "nginx:latest" -n cache | cut -d , -f 1)"
544
+# directly hit the image to trigger mirroring in case the layer already exists on disk
545
+os::cmd::expect_success "curl -H 'Authorization: bearer $(oc sa get-token builder -n cache)' 'http://${DOCKER_REGISTRY}/v2/cache/nginx/blobs/${nginxblob}' 1>/dev/null"
546
+# verify the blob exists on disk in the registry due to mirroring under .../blobs/sha256/<2 char prefix>/<sha value>
547
+os::cmd::try_until_success "oc exec --context='${CLUSTER_ADMIN_CONTEXT}' -n default -p '${registry_pod}' du /registry | tee '${LOG_DIR}/registry-images.txt' | grep '${nginxblob:7:100}' | grep blobs"
548
+os::cmd::expect_success "oc delete is nginx -n cache"
549
+os::cmd::expect_success "oc exec -p ${registry_pod} du /registry > '${LOG_DIR}/prune-images.before.txt'"
550
+os::cmd::expect_success_and_not_text "oadm prune images --token='$(oc sa get-token builder -n cache)' --keep-younger-than=0 --confirm --all --registry-url='${DOCKER_REGISTRY}'" 'error'
551
+os::cmd::expect_failure "oc exec --context='${CLUSTER_ADMIN_CONTEXT}' -n default -p '${registry_pod}' du /registry | tee '${LOG_DIR}/registry-images.txt' | grep '${nginxblob:7:100}' | grep blobs"
552
+os::cmd::expect_success "oc exec -p ${registry_pod} du /registry > '${LOG_DIR}/prune-images.after.txt'"
553
+os::cmd::expect_code "diff '${LOG_DIR}/prune-images.before.txt' '${LOG_DIR}/prune-images.after.txt'" 1
542 554
 os::log::info "Validated image pruning"
543 555
 
544 556
 # with registry's re-deployment we loose all the blobs stored in its storage until now