Browse code

Merge pull request #8874 from smarterclayton/release-1.2

Release v1.2.0 branch

Clayton Coleman authored on 2016/05/24 08:04:31
Showing 9 changed files
... ...
@@ -70,7 +70,7 @@ func (autoscalerStrategy) AllowCreateOnUpdate() bool {
70 70
 // PrepareForUpdate clears fields that are not allowed to be set by end users on update.
71 71
 func (autoscalerStrategy) PrepareForUpdate(obj, old runtime.Object) {
72 72
 	newHPA := obj.(*extensions.HorizontalPodAutoscaler)
73
-	oldHPA := obj.(*extensions.HorizontalPodAutoscaler)
73
+	oldHPA := old.(*extensions.HorizontalPodAutoscaler)
74 74
 	// Update is not allowed to set status
75 75
 	newHPA.Status = oldHPA.Status
76 76
 }
... ...
@@ -64,7 +64,7 @@ func (persistentvolumeStrategy) AllowCreateOnUpdate() bool {
64 64
 // PrepareForUpdate sets the Status fields which is not allowed to be set by an end user updating a PV
65 65
 func (persistentvolumeStrategy) PrepareForUpdate(obj, old runtime.Object) {
66 66
 	newPv := obj.(*api.PersistentVolume)
67
-	oldPv := obj.(*api.PersistentVolume)
67
+	oldPv := old.(*api.PersistentVolume)
68 68
 	newPv.Status = oldPv.Status
69 69
 }
70 70
 
... ...
@@ -86,7 +86,7 @@ var StatusStrategy = persistentvolumeStatusStrategy{Strategy}
86 86
 // PrepareForUpdate sets the Spec field which is not allowed to be changed when updating a PV's Status
87 87
 func (persistentvolumeStatusStrategy) PrepareForUpdate(obj, old runtime.Object) {
88 88
 	newPv := obj.(*api.PersistentVolume)
89
-	oldPv := obj.(*api.PersistentVolume)
89
+	oldPv := old.(*api.PersistentVolume)
90 90
 	newPv.Spec = oldPv.Spec
91 91
 }
92 92
 
... ...
@@ -64,7 +64,7 @@ func (persistentvolumeclaimStrategy) AllowCreateOnUpdate() bool {
64 64
 // PrepareForUpdate sets the Status field which is not allowed to be set by end users on update
65 65
 func (persistentvolumeclaimStrategy) PrepareForUpdate(obj, old runtime.Object) {
66 66
 	newPvc := obj.(*api.PersistentVolumeClaim)
67
-	oldPvc := obj.(*api.PersistentVolumeClaim)
67
+	oldPvc := old.(*api.PersistentVolumeClaim)
68 68
 	newPvc.Status = oldPvc.Status
69 69
 }
70 70
 
... ...
@@ -86,7 +86,7 @@ var StatusStrategy = persistentvolumeclaimStatusStrategy{Strategy}
86 86
 // PrepareForUpdate sets the Spec field which is not allowed to be changed when updating a PV's Status
87 87
 func (persistentvolumeclaimStatusStrategy) PrepareForUpdate(obj, old runtime.Object) {
88 88
 	newPv := obj.(*api.PersistentVolumeClaim)
89
-	oldPv := obj.(*api.PersistentVolumeClaim)
89
+	oldPv := old.(*api.PersistentVolumeClaim)
90 90
 	newPv.Spec = oldPv.Spec
91 91
 }
92 92
 
... ...
@@ -18,6 +18,7 @@ package storage
18 18
 
19 19
 import (
20 20
 	"fmt"
21
+	"net/http"
21 22
 	"reflect"
22 23
 	"strconv"
23 24
 	"strings"
... ...
@@ -25,8 +26,10 @@ import (
25 25
 	"time"
26 26
 
27 27
 	"k8s.io/kubernetes/pkg/api"
28
+	"k8s.io/kubernetes/pkg/api/errors"
28 29
 	"k8s.io/kubernetes/pkg/api/meta"
29 30
 	"k8s.io/kubernetes/pkg/api/rest"
31
+	"k8s.io/kubernetes/pkg/api/unversioned"
30 32
 	"k8s.io/kubernetes/pkg/client/cache"
31 33
 	"k8s.io/kubernetes/pkg/conversion"
32 34
 	"k8s.io/kubernetes/pkg/runtime"
... ...
@@ -264,7 +267,10 @@ func (c *Cacher) Watch(ctx context.Context, key string, resourceVersion string,
264 264
 	defer c.watchCache.RUnlock()
265 265
 	initEvents, err := c.watchCache.GetAllEventsSinceThreadUnsafe(watchRV)
266 266
 	if err != nil {
267
-		return nil, err
267
+		// To match the uncached watch implementation, once we have passed authn/authz/admission,
268
+		// and successfully parsed a resource version, other errors must fail with a watch event of type ERROR,
269
+		// rather than a directly returned error.
270
+		return newErrWatcher(err), nil
268 271
 	}
269 272
 
270 273
 	c.Lock()
... ...
@@ -460,6 +466,46 @@ func (lw *cacherListerWatcher) Watch(options api.ListOptions) (watch.Interface,
460 460
 	return lw.storage.WatchList(context.TODO(), lw.resourcePrefix, options.ResourceVersion, Everything)
461 461
 }
462 462
 
463
+// cacherWatch implements watch.Interface to return a single error
464
+type errWatcher struct {
465
+	result chan watch.Event
466
+}
467
+
468
+func newErrWatcher(err error) *errWatcher {
469
+	// Create an error event
470
+	errEvent := watch.Event{Type: watch.Error}
471
+	switch err := err.(type) {
472
+	case runtime.Object:
473
+		errEvent.Object = err
474
+	case *errors.StatusError:
475
+		errEvent.Object = &err.ErrStatus
476
+	default:
477
+		errEvent.Object = &unversioned.Status{
478
+			Status:  unversioned.StatusFailure,
479
+			Message: err.Error(),
480
+			Reason:  unversioned.StatusReasonInternalError,
481
+			Code:    http.StatusInternalServerError,
482
+		}
483
+	}
484
+
485
+	// Create a watcher with room for a single event, populate it, and close the channel
486
+	watcher := &errWatcher{result: make(chan watch.Event, 1)}
487
+	watcher.result <- errEvent
488
+	close(watcher.result)
489
+
490
+	return watcher
491
+}
492
+
493
+// Implements watch.Interface.
494
+func (c *errWatcher) ResultChan() <-chan watch.Event {
495
+	return c.result
496
+}
497
+
498
+// Implements watch.Interface.
499
+func (c *errWatcher) Stop() {
500
+	// no-op
501
+}
502
+
463 503
 // cacherWatch implements watch.Interface
464 504
 type cacheWatcher struct {
465 505
 	sync.Mutex
... ...
@@ -24,6 +24,7 @@ import (
24 24
 	"time"
25 25
 
26 26
 	"k8s.io/kubernetes/pkg/api"
27
+	"k8s.io/kubernetes/pkg/api/errors"
27 28
 	"k8s.io/kubernetes/pkg/api/meta"
28 29
 	"k8s.io/kubernetes/pkg/api/testapi"
29 30
 	apitesting "k8s.io/kubernetes/pkg/api/testing"
... ...
@@ -225,11 +226,15 @@ func TestWatch(t *testing.T) {
225 225
 	verifyWatchEvent(t, watcher, watch.Added, podFoo)
226 226
 	verifyWatchEvent(t, watcher, watch.Modified, podFooPrime)
227 227
 
228
-	// Check whether we get too-old error.
229
-	_, err = cacher.Watch(context.TODO(), "pods/ns/foo", "1", storage.Everything)
230
-	if err == nil {
231
-		t.Errorf("Expected 'error too old' error")
228
+	// Check whether we get too-old error via the watch channel
229
+	tooOldWatcher, err := cacher.Watch(context.TODO(), "pods/ns/foo", "1", storage.Everything)
230
+	if err != nil {
231
+		t.Fatalf("Expected no direct error, got %v", err)
232 232
 	}
233
+	defer tooOldWatcher.Stop()
234
+	// Ensure we get a "Gone" error
235
+	expectedGoneError := errors.NewGone("").(*errors.StatusError).ErrStatus
236
+	verifyWatchEvent(t, tooOldWatcher, watch.Error, &expectedGoneError)
233 237
 
234 238
 	initialWatcher, err := cacher.Watch(context.TODO(), "pods/ns/foo", fooCreated.ResourceVersion, storage.Everything)
235 239
 	if err != nil {
... ...
@@ -173,7 +173,7 @@ func importImages(ctx gocontext.Context, retriever RepositoryRetriever, isi *api
173 173
 			}
174 174
 			for _, index := range tags[j] {
175 175
 				if tag.Err != nil {
176
-					setImageImportStatus(isi, index, tag.Err)
176
+					setImageImportStatus(isi, index, tag.Name, tag.Err)
177 177
 					continue
178 178
 				}
179 179
 				copied := *tag.Image
... ...
@@ -194,7 +194,7 @@ func importImages(ctx gocontext.Context, retriever RepositoryRetriever, isi *api
194 194
 			}
195 195
 			for _, index := range ids[j] {
196 196
 				if digest.Err != nil {
197
-					setImageImportStatus(isi, index, digest.Err)
197
+					setImageImportStatus(isi, index, "", digest.Err)
198 198
 					continue
199 199
 				}
200 200
 				image := &isi.Status.Images[index]
... ...
@@ -267,6 +267,7 @@ func importFromRepository(ctx gocontext.Context, retriever RepositoryRetriever,
267 267
 	status.Status.Status = unversioned.StatusSuccess
268 268
 	status.Images = make([]api.ImageImportStatus, len(repo.Tags))
269 269
 	for i, tag := range repo.Tags {
270
+		status.Images[i].Tag = tag.Name
270 271
 		if tag.Err != nil {
271 272
 			failures++
272 273
 			status.Images[i].Status = imageImportStatus(tag.Err, "", "repository")
... ...
@@ -277,7 +278,6 @@ func importFromRepository(ctx gocontext.Context, retriever RepositoryRetriever,
277 277
 		copied := *tag.Image
278 278
 		ref.Tag, ref.ID = tag.Name, copied.Name
279 279
 		copied.DockerImageReference = ref.MostSpecific().Exact()
280
-		status.Images[i].Tag = tag.Name
281 280
 		status.Images[i].Image = &copied
282 281
 	}
283 282
 	if failures > 0 {
... ...
@@ -598,7 +598,8 @@ func imageImportStatus(err error, kind, position string) unversioned.Status {
598 598
 	}
599 599
 }
600 600
 
601
-func setImageImportStatus(images *api.ImageStreamImport, i int, err error) {
601
+func setImageImportStatus(images *api.ImageStreamImport, i int, tag string, err error) {
602
+	images.Status.Images[i].Tag = tag
602 603
 	images.Status.Images[i].Status = imageImportStatus(err, "", "")
603 604
 }
604 605
 
... ...
@@ -149,6 +149,12 @@ func TestImport(t *testing.T) {
149 149
 				if status := isi.Status.Images[3].Status; status.Status != "" {
150 150
 					t.Errorf("unexpected status: %#v", isi.Status.Images[3].Status)
151 151
 				}
152
+				expectedTags := []string{"latest", "", "", ""}
153
+				for i, image := range isi.Status.Images {
154
+					if image.Tag != expectedTags[i] {
155
+						t.Errorf("unexpected tag of status %d (%s != %s)", i, image.Tag, expectedTags[i])
156
+					}
157
+				}
152 158
 			},
153 159
 		},
154 160
 		{
... ...
@@ -186,6 +192,7 @@ func TestImport(t *testing.T) {
186 186
 				if len(isi.Status.Images) != 2 {
187 187
 					t.Errorf("unexpected number of images: %#v", isi.Status.Repository.Images)
188 188
 				}
189
+				expectedTags := []string{"", "tag"}
189 190
 				for i, image := range isi.Status.Images {
190 191
 					if image.Status.Status != unversioned.StatusSuccess {
191 192
 						t.Errorf("unexpected status %d: %#v", i, image.Status)
... ...
@@ -198,6 +205,9 @@ func TestImport(t *testing.T) {
198 198
 					if image.Image.DockerImageReference != "test@sha256:958608f8ecc1dc62c93b6c610f3a834dae4220c9642e6e8b4e0f2b3ad7cbd238" {
199 199
 						t.Errorf("unexpected ref %d: %#v", i, image.Image.DockerImageReference)
200 200
 					}
201
+					if image.Tag != expectedTags[i] {
202
+						t.Errorf("unexpected tag of status %d (%s != %s)", i, image.Tag, expectedTags[i])
203
+					}
201 204
 				}
202 205
 			},
203 206
 		},
... ...
@@ -222,10 +232,14 @@ func TestImport(t *testing.T) {
222 222
 				if len(isi.Status.Repository.Images) != 5 {
223 223
 					t.Errorf("unexpected number of images: %#v", isi.Status.Repository.Images)
224 224
 				}
225
+				expectedTags := []string{"3", "v2", "v1", "3.1", "abc"}
225 226
 				for i, image := range isi.Status.Repository.Images {
226 227
 					if image.Status.Status != unversioned.StatusFailure || image.Status.Message != "Internal error occurred: no such tag" {
227 228
 						t.Errorf("unexpected status %d: %#v", i, isi.Status.Repository.Images)
228 229
 					}
230
+					if image.Tag != expectedTags[i] {
231
+						t.Errorf("unexpected tag of status %d (%s != %s)", i, image.Tag, expectedTags[i])
232
+					}
229 233
 				}
230 234
 			},
231 235
 		},
... ...
@@ -177,6 +177,10 @@ func (r *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, err
177 177
 
178 178
 	if spec := isi.Spec.Repository; spec != nil {
179 179
 		for i, status := range isi.Status.Repository.Images {
180
+			if checkImportFailure(status, stream, status.Tag, nextGeneration, now) {
181
+				continue
182
+			}
183
+
180 184
 			image := status.Image
181 185
 			ref, err := api.ParseDockerImageReference(image.DockerImageReference)
182 186
 			if err != nil {
... ...
@@ -196,10 +200,6 @@ func (r *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, err
196 196
 			// we've imported a set of tags, ensure spec tag will point to this for later imports
197 197
 			from.ID, from.Tag = "", tag
198 198
 
199
-			if checkImportFailure(status, stream, tag, nextGeneration, now) {
200
-				continue
201
-			}
202
-
203 199
 			if updated, ok := r.importSuccessful(ctx, image, stream, tag, from.Exact(), nextGeneration, now, spec.ImportPolicy, importedImages, updatedImages); ok {
204 200
 				isi.Status.Repository.Images[i].Image = updated
205 201
 			}
... ...
@@ -278,6 +278,17 @@ func checkImportFailure(status api.ImageImportStatus, stream *api.ImageStream, t
278 278
 
279 279
 		LastTransitionTime: now,
280 280
 	}
281
+
282
+	if tag == "" {
283
+		if len(status.Tag) > 0 {
284
+			tag = status.Tag
285
+		} else if status.Image != nil {
286
+			if ref, err := api.ParseDockerImageReference(status.Image.DockerImageReference); err == nil {
287
+				tag = ref.Tag
288
+			}
289
+		}
290
+	}
291
+
281 292
 	if !api.HasTagCondition(stream, tag, condition) {
282 293
 		api.SetTagConditions(stream, tag, condition)
283 294
 		if tagRef, ok := stream.Spec.Tags[tag]; ok {
... ...
@@ -13,6 +13,7 @@ import (
13 13
 	"testing"
14 14
 	"time"
15 15
 
16
+	"github.com/docker/distribution/registry/api/errcode"
16 17
 	gocontext "golang.org/x/net/context"
17 18
 
18 19
 	kapi "k8s.io/kubernetes/pkg/api"
... ...
@@ -135,16 +136,26 @@ func TestImageStreamImport(t *testing.T) {
135 135
 	}
136 136
 }
137 137
 
138
-func mockRegistryHandler(t *testing.T, count *int) http.Handler {
138
+// mockRegistryHandler returns a registry mock handler with several repositories. requireAuth causes handler
139
+// to return unauthorized and request basic authentication header if not given. count is increased each
140
+// time the handler is invoked. There are three repositories:
141
+//  - test/image with phpManifest
142
+//  - test/image2 with etcdManifest
143
+//  - test/image3 with tags: v1, v2 and latest
144
+//    - the first points to etcdManifest
145
+//    - the others cause handler to return unknown error
146
+func mockRegistryHandler(t *testing.T, requireAuth bool, count *int) http.Handler {
139 147
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
140 148
 		(*count)++
141 149
 		t.Logf("%d got %s %s", *count, r.Method, r.URL.Path)
142 150
 
143 151
 		w.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
144
-		if len(r.Header.Get("Authorization")) == 0 {
145
-			w.Header().Set("WWW-Authenticate", "BASIC")
146
-			w.WriteHeader(http.StatusUnauthorized)
147
-			return
152
+		if requireAuth {
153
+			if len(r.Header.Get("Authorization")) == 0 {
154
+				w.Header().Set("WWW-Authenticate", "BASIC")
155
+				w.WriteHeader(http.StatusUnauthorized)
156
+				return
157
+			}
148 158
 		}
149 159
 
150 160
 		switch r.URL.Path {
... ...
@@ -154,6 +165,12 @@ func mockRegistryHandler(t *testing.T, count *int) http.Handler {
154 154
 			w.Write([]byte(phpManifest))
155 155
 		case "/v2/test/image2/manifests/" + etcdDigest:
156 156
 			w.Write([]byte(etcdManifest))
157
+		case "/v2/test/image3/tags/list":
158
+			w.Write([]byte("{\"name\": \"test/image3\", \"tags\": [\"latest\", \"v1\", \"v2\"]}"))
159
+		case "/v2/test/image3/manifests/latest", "/v2/test/image3/manifests/v2", "/v2/test/image3/manifests/" + danglingDigest:
160
+			errcode.ServeJSON(w, errcode.ErrorCodeUnknown)
161
+		case "/v2/test/image3/manifests/v1", "/v2/test/image3/manifests/" + etcdDigest:
162
+			w.Write([]byte(etcdManifest))
157 163
 		default:
158 164
 			t.Fatalf("unexpected request %s: %#v", r.URL.Path, r)
159 165
 		}
... ...
@@ -164,7 +181,7 @@ func TestImageStreamImportAuthenticated(t *testing.T) {
164 164
 	testutil.RequireEtcd(t)
165 165
 	// start regular HTTP servers
166 166
 	count := 0
167
-	server := httptest.NewServer(mockRegistryHandler(t, &count))
167
+	server := httptest.NewServer(mockRegistryHandler(t, true, &count))
168 168
 	server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
169 169
 		w.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
170 170
 		if len(r.Header.Get("Authorization")) == 0 {
... ...
@@ -177,7 +194,7 @@ func TestImageStreamImportAuthenticated(t *testing.T) {
177 177
 
178 178
 	// start a TLS server
179 179
 	count2 := 0
180
-	server3 := httptest.NewTLSServer(mockRegistryHandler(t, &count2))
180
+	server3 := httptest.NewTLSServer(mockRegistryHandler(t, true, &count2))
181 181
 
182 182
 	url1, _ := url.Parse(server.URL)
183 183
 	url2, _ := url.Parse(server2.URL)
... ...
@@ -324,6 +341,105 @@ func TestImageStreamImportAuthenticated(t *testing.T) {
324 324
 	}
325 325
 }
326 326
 
327
+// Verifies that individual errors for particular tags are handled properly when pulling all tags from a
328
+// repository.
329
+func TestImageStreamImportTagsFromRepository(t *testing.T) {
330
+	testutil.RequireEtcd(t)
331
+	// start regular HTTP servers
332
+	count := 0
333
+	server := httptest.NewServer(mockRegistryHandler(t, false, &count))
334
+
335
+	url, _ := url.Parse(server.URL)
336
+
337
+	// start a master
338
+	_, clusterAdminKubeConfig, err := testserver.StartTestMaster()
339
+	if err != nil {
340
+		t.Fatalf("unexpected error: %v", err)
341
+	}
342
+	/*
343
+		_, err := testutil.GetClusterAdminKubeClient(clusterAdminKubeConfig)
344
+		if err != nil {
345
+			t.Fatalf("unexpected error: %v", err)
346
+		}
347
+	*/
348
+	c, err := testutil.GetClusterAdminClient(clusterAdminKubeConfig)
349
+	if err != nil {
350
+		t.Fatalf("unexpected error: %v", err)
351
+	}
352
+	err = testutil.CreateNamespace(clusterAdminKubeConfig, testutil.Namespace())
353
+	if err != nil {
354
+		t.Fatalf("unexpected error: %v", err)
355
+	}
356
+
357
+	importSpec := &api.ImageStreamImport{
358
+		ObjectMeta: kapi.ObjectMeta{Name: "test"},
359
+		Spec: api.ImageStreamImportSpec{
360
+			Import: true,
361
+			Repository: &api.RepositoryImportSpec{
362
+				From:            kapi.ObjectReference{Kind: "DockerImage", Name: url.Host + "/test/image3"},
363
+				ImportPolicy:    api.TagImportPolicy{Insecure: true},
364
+				IncludeManifest: true,
365
+			},
366
+		},
367
+	}
368
+
369
+	// import expecting regular image to pass
370
+	isi, err := c.ImageStreams(testutil.Namespace()).Import(importSpec)
371
+	if err != nil {
372
+		t.Fatal(err)
373
+	}
374
+	if len(isi.Status.Images) != 0 {
375
+		t.Errorf("imported unexpected number of images (%d != 0)", len(isi.Status.Images))
376
+	}
377
+	if isi.Status.Repository == nil {
378
+		t.Fatalf("exported non-nil repository status")
379
+	}
380
+	if len(isi.Status.Repository.Images) != 3 {
381
+		t.Fatalf("imported unexpected number of tags (%d != 3)", len(isi.Status.Repository.Images))
382
+	}
383
+	for i, image := range isi.Status.Repository.Images {
384
+		switch i {
385
+		case 2:
386
+			if image.Status.Status != unversioned.StatusSuccess {
387
+				t.Errorf("import of image %d did not succeed: %#v", i, image.Status)
388
+			}
389
+			if image.Tag != "v1" {
390
+				t.Errorf("unexpected tag at position %d (%s != v1)", i, image.Tag)
391
+			}
392
+			if image.Image == nil {
393
+				t.Fatalf("expected image to be set")
394
+			}
395
+			if image.Image.DockerImageReference != url.Host+"/test/image3@"+etcdDigest {
396
+				t.Errorf("unexpected DockerImageReference (%s != %s)", image.Image.DockerImageReference, url.Host+"/test/image3@"+etcdDigest)
397
+			}
398
+			if image.Image.Name != etcdDigest {
399
+				t.Errorf("expected etcd digest as a name of the image (%s != %s)", image.Image.Name, etcdDigest)
400
+			}
401
+		default:
402
+			if image.Status.Status != unversioned.StatusFailure || image.Status.Reason != unversioned.StatusReasonInternalError {
403
+				t.Fatalf("import of image %d did not report internal server error: %#v", i, image.Status)
404
+			}
405
+			expectedTags := []string{"latest", "v2"}[i]
406
+			if image.Tag != expectedTags {
407
+				t.Errorf("unexpected tag at position %d (%s != %s)", i, image.Tag, expectedTags[i])
408
+			}
409
+		}
410
+	}
411
+
412
+	is, err := c.ImageStreams(testutil.Namespace()).Get("test")
413
+	if err != nil {
414
+		t.Fatal(err)
415
+	}
416
+	tagEvent := api.LatestTaggedImage(is, "v1")
417
+	if tagEvent == nil {
418
+		t.Fatalf("no image tagged for v1: %#v", is)
419
+	}
420
+
421
+	if tagEvent == nil || tagEvent.Image != etcdDigest || tagEvent.DockerImageReference != url.Host+"/test/image3@"+etcdDigest {
422
+		t.Fatalf("expected the etcd image to be tagged: %#v", tagEvent)
423
+	}
424
+}
425
+
327 426
 // Verifies that the import scheduler fetches an image repeatedly (every 1s as per the default
328 427
 // test controller interval), updates the image stream only when there are changes, and if an
329 428
 // error occurs writes the error only once (instead of every interval)
... ...
@@ -861,3 +977,5 @@ const phpManifest = `{
861 861
       }
862 862
    ]
863 863
 }`
864
+
865
+const danglingDigest = `sha256:f374c0d9b59e6fdf9f8922d59e946b05fbeabaed70b0639d7b6b524f3299e87b`