@specific.dev/spectest 0.131.8 → 0.131.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/k3s-join.d.ts +2 -0
- package/dist/components/k3s-join.js +13 -0
- package/dist/components/k3s.js +3 -5
- package/dist/components/s3-runtime/README.md +6 -0
- package/dist/components/s3-runtime/main_test.go +122 -32
- package/dist/components/s3-runtime/multipart.patch +196 -0
- package/package.json +1 -1
- package/src/components/k3s-docker.test.ts +7 -0
- package/src/components/k3s-join.test.ts +38 -0
- package/src/components/k3s-join.ts +17 -0
- package/src/components/k3s-nodes.test.ts +1 -1
- package/src/components/k3s.ts +3 -5
- package/src/components/s3-runtime/README.md +6 -0
- package/src/components/s3-runtime/main_test.go +122 -32
- package/src/components/s3-runtime/multipart.patch +196 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const JOIN_DIRECTORY = "/etc/spectest-k3s-join";
|
|
2
|
+
// The join volume is private to one cluster and is captured with its network.
|
|
3
|
+
// Use the server's numeric address from the beginning: K3s otherwise demotes
|
|
4
|
+
// the DNS fallback after discovery and closes in-flight bootstrap requests.
|
|
5
|
+
export function prepareK3sJoin(server, directory = JOIN_DIRECTORY) {
|
|
6
|
+
if (server)
|
|
7
|
+
return ('SERVER="$(ip -4 route get 1.1.1.1 | awk \'{for (i=1;i<NF;i++) if ($i=="src") {print $(i+1); exit}}\')"; ' +
|
|
8
|
+
'case "$SERVER" in ""|*[!0-9.]*) echo "spectest: missing server IPv4 address" >&2; exit 1;; esac; ' +
|
|
9
|
+
`printf '%s\\n' "$SERVER" > ${directory}/server.tmp && mv ${directory}/server.tmp ${directory}/server || exit 1; ` +
|
|
10
|
+
`if [ ! -s ${directory}/token ]; then (umask 077; cat /proc/sys/kernel/random/uuid > ${directory}/token) || exit 1; fi; `);
|
|
11
|
+
return (`while [ ! -s ${directory}/token ] || [ ! -s ${directory}/server ]; do sleep 0.1; done; ` +
|
|
12
|
+
`SERVER="$(cat ${directory}/server)"; `);
|
|
13
|
+
}
|
package/dist/components/k3s.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isIP } from "node:net";
|
|
2
2
|
import { waitForK3sNodes } from "./k3s-nodes.js";
|
|
3
|
+
import { JOIN_DIRECTORY, prepareK3sJoin } from "./k3s-join.js";
|
|
3
4
|
import { installPodTrust } from "./k3s-trust.js";
|
|
4
5
|
import { k3sCoverage } from "./k3s-coverage.js";
|
|
5
6
|
import { waitForAll } from "../harness/concurrency.js";
|
|
@@ -21,7 +22,6 @@ import { deepUnwrap, readTag, wrap } from "../inspect.js";
|
|
|
21
22
|
import { recorderAnnotate, recorderRemove } from "../recorder.js";
|
|
22
23
|
/** Port the in-cluster registry listens on (plain HTTP). */
|
|
23
24
|
const K3S_REGISTRY_PORT = 5000;
|
|
24
|
-
const JOIN_DIRECTORY = "/etc/spectest-k3s-join";
|
|
25
25
|
/** The cluster's own containerd root, inside the k3s container. */
|
|
26
26
|
const CLUSTER_STORE_TARGET = "/var/lib/rancher/k3s/agent/containerd";
|
|
27
27
|
/** The socket of the throwaway daemon that prepares the store. */
|
|
@@ -1374,7 +1374,7 @@ function createK3sCluster(opts, count) {
|
|
|
1374
1374
|
"--kubelet-arg=kubelet-cgroups=/spectest-$(hostname)/k3s",
|
|
1375
1375
|
"--kubelet-arg=runtime-cgroups=/spectest-$(hostname)/k3s",
|
|
1376
1376
|
`--token-file=${JOIN_DIRECTORY}/token`,
|
|
1377
|
-
...(server ? [] : [
|
|
1377
|
+
...(server ? [] : ['--server="https://$SERVER:6443"']),
|
|
1378
1378
|
// CoreDNS / pod DNS upstream. Without this, k3s sees only the
|
|
1379
1379
|
// loopback 127.0.0.11 (Docker's embedded DNS) in the container's
|
|
1380
1380
|
// /etc/resolv.conf, decides no usable nameserver exists, and writes a
|
|
@@ -1464,9 +1464,7 @@ function createK3sCluster(opts, count) {
|
|
|
1464
1464
|
'sed "s/[^ ][^ ]*/+&/g" "/sys/fs/cgroup/$CG/cgroup.controllers" > "/sys/fs/cgroup/$CG/cgroup.subtree_control" || exit 1; ' +
|
|
1465
1465
|
'else for C in /sys/fs/cgroup/*; do [ ! -d "$C" ] || mkdir -p "$C/$CG/k3s" || exit 1; done; fi; ' +
|
|
1466
1466
|
storePrepareSh() +
|
|
1467
|
-
(server
|
|
1468
|
-
? `if [ ! -s ${JOIN_DIRECTORY}/token ]; then (umask 077; cat /proc/sys/kernel/random/uuid > ${JOIN_DIRECTORY}/token); fi; `
|
|
1469
|
-
: `while [ ! -s ${JOIN_DIRECTORY}/token ]; do sleep 0.1; done; `) +
|
|
1467
|
+
prepareK3sJoin(server) +
|
|
1470
1468
|
// Sticky and environment-local stores use overlayfs.
|
|
1471
1469
|
`exec ${nodeArgs} --snapshotter=$SNAP`;
|
|
1472
1470
|
// Plain /readyz probe. On a warm zot cache the cluster's images are
|
|
@@ -27,6 +27,12 @@ one backend snapshot, including closed byte ranges, exact ETag conditions,
|
|
|
27
27
|
version selection and SHA256 part receipts. Date-based copy conditions are
|
|
28
28
|
explicitly unsupported (501), rather than silently ignored.
|
|
29
29
|
|
|
30
|
+
Multipart completion stores its opaque ETag on the same in-memory object version
|
|
31
|
+
as the bytes. HEAD/GET, listings, copy conditions and atomic write conditions use
|
|
32
|
+
that tag; ordinary PUT and CopyObject retain their own payload-derived tags.
|
|
33
|
+
Versioned HEAD reads the requested version. The optional backend write port keeps
|
|
34
|
+
completion tags out of client metadata and avoids a separate post-write update.
|
|
35
|
+
|
|
30
36
|
Run `./test.sh` from this directory to exercise real HTTP requests against the
|
|
31
37
|
same patched sources used in the image, including corrupt retries, conditional
|
|
32
38
|
completion races, abort, metadata reconciliation and unchecksummed uploads.
|
|
@@ -147,39 +147,54 @@ func TestMultipartSHA256IntegrityAndReconciliation(t *testing.T) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
func TestMultipartConditionalCompletionIsAtomic(t *testing.T) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
statuses <- 0
|
|
164
|
-
return
|
|
150
|
+
for _, replace := range []bool{false, true} {
|
|
151
|
+
t.Run(fmt.Sprint(replace), func(t *testing.T) {
|
|
152
|
+
p := newProbe(t)
|
|
153
|
+
header, tag := "If-None-Match", "*"
|
|
154
|
+
if replace {
|
|
155
|
+
seed := p.start("same", true)
|
|
156
|
+
part := p.part(seed, 1, []byte("original"), true)
|
|
157
|
+
response := p.request("POST", seed, completion(part), nil, 200)
|
|
158
|
+
var receipt struct{ ETag string }
|
|
159
|
+
if err := xml.Unmarshal(response.body, &receipt); err != nil {
|
|
160
|
+
t.Fatal(err)
|
|
161
|
+
}
|
|
162
|
+
header, tag = "If-Match", receipt.ETag
|
|
165
163
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
statuses
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
164
|
+
paths := []string{p.start("same", true), p.start("same", true)}
|
|
165
|
+
parts := []string{p.part(paths[0], 1, []byte("first"), true), p.part(paths[1], 1, []byte("second"), true)}
|
|
166
|
+
statuses := make(chan int, 2)
|
|
167
|
+
var wg sync.WaitGroup
|
|
168
|
+
for i := range paths {
|
|
169
|
+
wg.Add(1)
|
|
170
|
+
go func(i int) {
|
|
171
|
+
defer wg.Done()
|
|
172
|
+
req, _ := http.NewRequest("POST", p.server.URL+paths[i], bytes.NewReader(completion(parts[i])))
|
|
173
|
+
req.Header.Set(header, tag)
|
|
174
|
+
res, err := p.server.Client().Do(req)
|
|
175
|
+
if err != nil {
|
|
176
|
+
statuses <- 0
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
defer res.Body.Close()
|
|
180
|
+
io.Copy(io.Discard, res.Body)
|
|
181
|
+
statuses <- res.StatusCode
|
|
182
|
+
}(i)
|
|
183
|
+
}
|
|
184
|
+
wg.Wait()
|
|
185
|
+
close(statuses)
|
|
186
|
+
counts := map[int]int{}
|
|
187
|
+
for s := range statuses {
|
|
188
|
+
counts[s]++
|
|
189
|
+
}
|
|
190
|
+
if counts[200] != 1 || counts[412] != 1 {
|
|
191
|
+
t.Fatalf("competing creates: %v", counts)
|
|
192
|
+
}
|
|
193
|
+
body := p.request("GET", "/uploads/same", nil, nil, 200).body
|
|
194
|
+
if string(body) != "first" && string(body) != "second" {
|
|
195
|
+
t.Fatalf("bad winner: %s", body)
|
|
196
|
+
}
|
|
197
|
+
})
|
|
183
198
|
}
|
|
184
199
|
}
|
|
185
200
|
|
|
@@ -280,3 +295,78 @@ func TestUploadPartCopyVersionChecksumAndAbort(t *testing.T) {
|
|
|
280
295
|
p.request("PUT", path+"&partNumber=1", nil, headers, 404)
|
|
281
296
|
p.request("DELETE", path, nil, nil, 204)
|
|
282
297
|
}
|
|
298
|
+
|
|
299
|
+
func TestMultipartSourceEntityTagSurvivesCopyVersionsAndOverwrite(t *testing.T) {
|
|
300
|
+
for _, count := range []int{1, 2} {
|
|
301
|
+
t.Run(fmt.Sprint(count), func(t *testing.T) {
|
|
302
|
+
p := newProbe(t)
|
|
303
|
+
p.request("PUT", "/uploads?versioning", []byte("<VersioningConfiguration><Status>Enabled</Status></VersioningConfiguration>"), nil, 200)
|
|
304
|
+
body := bytes.Repeat([]byte("a"), 6*1024*1024)
|
|
305
|
+
path := p.start("staged", false)
|
|
306
|
+
parts := []string{}
|
|
307
|
+
if count == 1 {
|
|
308
|
+
parts = append(parts, p.part(path, 1, body, false))
|
|
309
|
+
} else {
|
|
310
|
+
parts = append(parts, p.part(path, 1, body[:5*1024*1024], false), p.part(path, 2, body[5*1024*1024:], false))
|
|
311
|
+
}
|
|
312
|
+
complete := p.request("POST", path, completion(parts...), nil, 200)
|
|
313
|
+
var receipt struct{ ETag string }
|
|
314
|
+
if err := xml.Unmarshal(complete.body, &receipt); err != nil || !strings.Contains(receipt.ETag, fmt.Sprintf("-%d", count)) {
|
|
315
|
+
t.Fatalf("completion tag: %s: %v", complete.body, err)
|
|
316
|
+
}
|
|
317
|
+
version := complete.header.Get("x-amz-version-id")
|
|
318
|
+
if version == "" {
|
|
319
|
+
t.Fatal("missing completed version")
|
|
320
|
+
}
|
|
321
|
+
destination := p.start("copied-staging", false)
|
|
322
|
+
headers := map[string]string{"x-amz-copy-source": "/uploads/staged", "x-amz-copy-source-if-match": receipt.ETag, "x-amz-copy-source-range": fmt.Sprintf("bytes=0-%d", len(body)-1)}
|
|
323
|
+
copied := p.request("PUT", destination+"&partNumber=1", nil, headers, 200)
|
|
324
|
+
var part struct{ ETag string }
|
|
325
|
+
if err := xml.Unmarshal(copied.body, &part); err != nil {
|
|
326
|
+
t.Fatal(err)
|
|
327
|
+
}
|
|
328
|
+
for _, method := range []string{"HEAD", "GET"} {
|
|
329
|
+
got := p.request(method, "/uploads/staged", nil, nil, 200)
|
|
330
|
+
if got.header.Get("ETag") != receipt.ETag {
|
|
331
|
+
t.Fatalf("%s lost completion tag: %s", method, got.header.Get("ETag"))
|
|
332
|
+
}
|
|
333
|
+
p.request(method, "/uploads/staged", nil, map[string]string{"If-None-Match": receipt.ETag}, 304)
|
|
334
|
+
}
|
|
335
|
+
for _, query := range []string{"?list-type=2", "?versions"} {
|
|
336
|
+
got := p.request("GET", "/uploads"+query, nil, nil, 200)
|
|
337
|
+
var list struct {
|
|
338
|
+
Contents []struct{ ETag string }
|
|
339
|
+
Version []struct{ ETag string }
|
|
340
|
+
}
|
|
341
|
+
if err := xml.Unmarshal(got.body, &list); err != nil {
|
|
342
|
+
t.Fatal(err)
|
|
343
|
+
}
|
|
344
|
+
tags := []string{}
|
|
345
|
+
for _, row := range list.Contents {
|
|
346
|
+
tags = append(tags, row.ETag)
|
|
347
|
+
}
|
|
348
|
+
for _, row := range list.Version {
|
|
349
|
+
tags = append(tags, row.ETag)
|
|
350
|
+
}
|
|
351
|
+
if len(tags) != 1 || tags[0] != receipt.ETag {
|
|
352
|
+
t.Fatalf("list lost tag: %v", tags)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
put := p.request("PUT", "/uploads/staged", []byte("replacement"), map[string]string{"If-Match": receipt.ETag}, 200)
|
|
356
|
+
if put.header.Get("ETag") == receipt.ETag {
|
|
357
|
+
t.Fatal("normal overwrite inherited multipart tag")
|
|
358
|
+
}
|
|
359
|
+
p.request("PUT", destination+"&partNumber=1", nil, headers, 412)
|
|
360
|
+
headers["x-amz-copy-source"] += "?versionId=" + version
|
|
361
|
+
p.request("PUT", destination+"&partNumber=1", nil, headers, 200)
|
|
362
|
+
head := p.request("HEAD", "/uploads/staged?versionId="+version, nil, nil, 200)
|
|
363
|
+
if head.header.Get("ETag") != receipt.ETag {
|
|
364
|
+
t.Fatal("version lost its tag")
|
|
365
|
+
}
|
|
366
|
+
p.request("POST", destination, completion(fmt.Sprintf("<Part><PartNumber>1</PartNumber><ETag>%s</ETag></Part>", part.ETag)), nil, 200)
|
|
367
|
+
if got := p.request("GET", "/uploads/copied-staging", nil, nil, 200); !bytes.Equal(got.body, body) {
|
|
368
|
+
t.Fatal("copied multipart staging bytes changed")
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -350,3 +350,199 @@
|
|
|
350
350
|
+ ChecksumSHA256 string `xml:"ChecksumSHA256"`
|
|
351
351
|
+ }{ETag: etag, LastModified: NewContentTime(g.timeSource.Now()), ChecksumSHA256: base64.StdEncoding.EncodeToString(digest.Sum(nil))})
|
|
352
352
|
+}
|
|
353
|
+
--- a/vendor/github.com/johannesboyne/gofakes3/backend.go
|
|
354
|
+
+++ b/vendor/github.com/johannesboyne/gofakes3/backend.go
|
|
355
|
+
@@ -23,7 +23,9 @@
|
|
356
|
+
Size int64
|
|
357
|
+
Contents io.ReadCloser
|
|
358
|
+
Hash []byte
|
|
359
|
+
- Range *ObjectRange
|
|
360
|
+
+ // EntityTag is the stored opaque tag; Hash remains the payload MD5.
|
|
361
|
+
+ EntityTag string
|
|
362
|
+
+ Range *ObjectRange
|
|
363
|
+
|
|
364
|
+
// VersionID will be empty if bucket versioning has not been enabled.
|
|
365
|
+
VersionID VersionID
|
|
366
|
+
@@ -31,6 +33,21 @@
|
|
367
|
+
// If versioning is enabled for the bucket, this is true if this object version
|
|
368
|
+
// is a delete marker.
|
|
369
|
+
IsDeleteMarker bool
|
|
370
|
+
+}
|
|
371
|
+
+
|
|
372
|
+
+// ETag returns the version-owned tag, retaining compatibility with backends
|
|
373
|
+
+// that only supply payload hashes.
|
|
374
|
+
+func (object *Object) ETag() string {
|
|
375
|
+
+ if object.EntityTag != "" {
|
|
376
|
+
+ return object.EntityTag
|
|
377
|
+
+ }
|
|
378
|
+
+ return FormatETag(object.Hash)
|
|
379
|
+
+}
|
|
380
|
+
+
|
|
381
|
+
+// MultipartObjectBackend atomically stores completion bytes, metadata and ETag
|
|
382
|
+
+// under the same conditional-write lock. The tag never enters client metadata.
|
|
383
|
+
+type MultipartObjectBackend interface {
|
|
384
|
+
+ PutMultipartObject(bucket, key string, metadata map[string]string, input io.Reader, size int64, tag string, conditions *PutConditions) (PutObjectResult, error)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
type ObjectList struct {
|
|
388
|
+
@@ -150,7 +167,8 @@
|
|
389
|
+
|
|
390
|
+
// Hash is the MD5 hash of the object content (used for ETag comparison)
|
|
391
|
+
// Only required if Exists is true
|
|
392
|
+
- Hash []byte
|
|
393
|
+
+ Hash []byte
|
|
394
|
+
+ EntityTag string
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ObjectInfo is a deprecated alias for ConditionalObjectInfo.
|
|
398
|
+
@@ -182,7 +200,10 @@
|
|
399
|
+
if len(expectedETag) >= 2 && expectedETag[0] == '"' && expectedETag[len(expectedETag)-1] == '"' {
|
|
400
|
+
expectedETag = expectedETag[1 : len(expectedETag)-1]
|
|
401
|
+
}
|
|
402
|
+
- actualETag := hex.EncodeToString(objectInfo.Hash)
|
|
403
|
+
+ actualETag := strings.Trim(objectInfo.EntityTag, "\"")
|
|
404
|
+
+ if actualETag == "" {
|
|
405
|
+
+ actualETag = hex.EncodeToString(objectInfo.Hash)
|
|
406
|
+
+ }
|
|
407
|
+
if expectedETag != actualETag {
|
|
408
|
+
return ErrorMessage(ErrPreconditionFailed, "The ETag does not match")
|
|
409
|
+
}
|
|
410
|
+
--- a/vendor/github.com/johannesboyne/gofakes3/uploader.go
|
|
411
|
+
+++ b/vendor/github.com/johannesboyne/gofakes3/uploader.go
|
|
412
|
+
@@ -496,7 +496,12 @@
|
|
413
|
+
input.ChecksumSHA256 = ""
|
|
414
|
+
input.ChecksumType = ""
|
|
415
|
+
}
|
|
416
|
+
- result, err := u.storage.PutObject(bucket, object, mpu.Meta, bytes.NewReader(body), int64(len(body)), input.Conditions)
|
|
417
|
+
+ var result PutObjectResult
|
|
418
|
+
+ if backend, ok := u.storage.(MultipartObjectBackend); ok {
|
|
419
|
+
+ result, err = backend.PutMultipartObject(bucket, object, mpu.Meta, bytes.NewReader(body), int64(len(body)), etag, input.Conditions)
|
|
420
|
+
+ } else {
|
|
421
|
+
+ result, err = u.storage.PutObject(bucket, object, mpu.Meta, bytes.NewReader(body), int64(len(body)), input.Conditions)
|
|
422
|
+
+ }
|
|
423
|
+
if err != nil {
|
|
424
|
+
return "", "", err
|
|
425
|
+
}
|
|
426
|
+
--- a/vendor/github.com/johannesboyne/gofakes3/gofakes3.go
|
|
427
|
+
+++ b/vendor/github.com/johannesboyne/gofakes3/gofakes3.go
|
|
428
|
+
@@ -535,7 +535,7 @@
|
|
429
|
+
w.Header().Set("x-amz-version-id", string(obj.VersionID))
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
- etag := `"` + hex.EncodeToString(obj.Hash) + `"`
|
|
433
|
+
+ etag := obj.ETag()
|
|
434
|
+
w.Header().Set("ETag", etag)
|
|
435
|
+
|
|
436
|
+
if r.Header.Get("If-None-Match") == etag {
|
|
437
|
+
@@ -576,7 +576,16 @@
|
|
438
|
+
return err
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
- obj, err := g.storage.HeadObject(bucket, object)
|
|
442
|
+
+ var obj *Object
|
|
443
|
+
+ var err error
|
|
444
|
+
+ if versionID == "" {
|
|
445
|
+
+ obj, err = g.storage.HeadObject(bucket, object)
|
|
446
|
+
+ } else {
|
|
447
|
+
+ if g.versioned == nil {
|
|
448
|
+
+ return ErrNotImplemented
|
|
449
|
+
+ }
|
|
450
|
+
+ obj, err = g.versioned.HeadObjectVersion(bucket, object, versionID)
|
|
451
|
+
+ }
|
|
452
|
+
if err != nil {
|
|
453
|
+
return err
|
|
454
|
+
}
|
|
455
|
+
--- a/vendor/github.com/johannesboyne/gofakes3/upload_part_copy.go
|
|
456
|
+
+++ b/vendor/github.com/johannesboyne/gofakes3/upload_part_copy.go
|
|
457
|
+
@@ -56,7 +56,7 @@
|
|
458
|
+
if src.IsDeleteMarker {
|
|
459
|
+
return ErrInvalidArgument
|
|
460
|
+
}
|
|
461
|
+
- tag := FormatETag(src.Hash)
|
|
462
|
+
+ tag := src.ETag()
|
|
463
|
+
if expected := r.Header.Get("x-amz-copy-source-if-match"); expected != "" && expected != tag {
|
|
464
|
+
return ErrPreconditionFailed
|
|
465
|
+
}
|
|
466
|
+
--- a/vendor/github.com/johannesboyne/gofakes3/backend/s3mem/backend.go
|
|
467
|
+
+++ b/vendor/github.com/johannesboyne/gofakes3/backend/s3mem/backend.go
|
|
468
|
+
@@ -119,7 +119,7 @@
|
|
469
|
+
response.Add(&gofakes3.Content{
|
|
470
|
+
Key: item.data.name,
|
|
471
|
+
LastModified: gofakes3.NewContentTime(item.data.lastModified),
|
|
472
|
+
- ETag: gofakes3.FormatETag(item.data.hash),
|
|
473
|
+
+ ETag: item.data.entityTag,
|
|
474
|
+
Size: int64(len(item.data.body)),
|
|
475
|
+
})
|
|
476
|
+
}
|
|
477
|
+
@@ -240,6 +240,16 @@
|
|
478
|
+
size int64,
|
|
479
|
+
conditions *gofakes3.PutConditions,
|
|
480
|
+
) (result gofakes3.PutObjectResult, err error) {
|
|
481
|
+
+ return db.putObject(bucketName, objectName, meta, input, size, "", conditions)
|
|
482
|
+
+}
|
|
483
|
+
+
|
|
484
|
+
+// PutMultipartObject retains the completion tag in the same object version as
|
|
485
|
+
+// its bytes and uses the ordinary atomic conditional-write admission.
|
|
486
|
+
+func (db *Backend) PutMultipartObject(bucketName, objectName string, meta map[string]string, input io.Reader, size int64, tag string, conditions *gofakes3.PutConditions) (gofakes3.PutObjectResult, error) {
|
|
487
|
+
+ return db.putObject(bucketName, objectName, meta, input, size, tag, conditions)
|
|
488
|
+
+}
|
|
489
|
+
+
|
|
490
|
+
+func (db *Backend) putObject(bucketName, objectName string, meta map[string]string, input io.Reader, size int64, tag string, conditions *gofakes3.PutConditions) (result gofakes3.PutObjectResult, err error) {
|
|
491
|
+
// No need to lock the backend while we read the data into memory; it holds
|
|
492
|
+
// the write lock open unnecessarily, and could be blocked for an unreasonably
|
|
493
|
+
// long time by a connection timing out:
|
|
494
|
+
@@ -272,11 +282,15 @@
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
hash := md5.Sum(bts)
|
|
498
|
+
+ if tag == "" {
|
|
499
|
+
+ tag = gofakes3.FormatETag(hash[:])
|
|
500
|
+
+ }
|
|
501
|
+
|
|
502
|
+
item := &bucketData{
|
|
503
|
+
name: objectName,
|
|
504
|
+
body: bts,
|
|
505
|
+
hash: hash[:],
|
|
506
|
+
+ entityTag: tag,
|
|
507
|
+
metadata: meta,
|
|
508
|
+
lastModified: db.timeSource.Now(),
|
|
509
|
+
}
|
|
510
|
+
@@ -557,7 +571,7 @@
|
|
511
|
+
IsLatest: version == object.data,
|
|
512
|
+
LastModified: gofakes3.NewContentTime(version.lastModified),
|
|
513
|
+
Size: int64(len(version.body)),
|
|
514
|
+
- ETag: gofakes3.FormatETag(version.hash),
|
|
515
|
+
+ ETag: version.entityTag,
|
|
516
|
+
}
|
|
517
|
+
if bucket.versioning != gofakes3.VersioningNone { // S300005
|
|
518
|
+
resultVer.VersionID = version.versionID
|
|
519
|
+
@@ -587,8 +601,9 @@
|
|
520
|
+
return &gofakes3.ConditionalObjectInfo{Exists: false}, nil
|
|
521
|
+
}
|
|
522
|
+
return &gofakes3.ConditionalObjectInfo{
|
|
523
|
+
- Exists: true,
|
|
524
|
+
- Hash: existing.data.hash,
|
|
525
|
+
+ Exists: true,
|
|
526
|
+
+ Hash: existing.data.hash,
|
|
527
|
+
+ EntityTag: existing.data.entityTag,
|
|
528
|
+
}, nil
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
--- a/vendor/github.com/johannesboyne/gofakes3/backend/s3mem/bucket.go
|
|
532
|
+
+++ b/vendor/github.com/johannesboyne/gofakes3/backend/s3mem/bucket.go
|
|
533
|
+
@@ -118,6 +118,7 @@
|
|
534
|
+
deleteMarker bool
|
|
535
|
+
body []byte
|
|
536
|
+
hash []byte
|
|
537
|
+
+ entityTag string
|
|
538
|
+
metadata map[string]string
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
@@ -150,6 +151,7 @@
|
|
542
|
+
return &gofakes3.Object{
|
|
543
|
+
Name: bi.name,
|
|
544
|
+
Hash: bi.hash,
|
|
545
|
+
+ EntityTag: bi.entityTag,
|
|
546
|
+
Metadata: bi.metadata,
|
|
547
|
+
Size: sz,
|
|
548
|
+
Range: rnge,
|
package/package.json
CHANGED
|
@@ -65,6 +65,13 @@ for (const count of [1, 3]) test.skipIf(!process.env.SPECTEST_TEST_K3S_DOCKER)(`
|
|
|
65
65
|
await Bun.sleep(250);
|
|
66
66
|
}
|
|
67
67
|
await primary.setup!({ name, helpers, exec } as unknown as ServiceSetupContext<K3sHelpers>);
|
|
68
|
+
// Discovery must retain the bootstrap address. A DNS fallback is demoted
|
|
69
|
+
// to standby and closes active requests, which can kill initial Flannel
|
|
70
|
+
// authorization even though the API server remains healthy.
|
|
71
|
+
for (const node of names.filter(node => node !== name)) {
|
|
72
|
+
const config = JSON.parse(await checked(["docker", "exec", node, "cat", "/var/lib/rancher/k3s/agent/etc/k3s-agent-load-balancer.json"]));
|
|
73
|
+
expect(config.ServerAddresses).toContain(new URL(config.ServerURL).host);
|
|
74
|
+
}
|
|
68
75
|
await helpers.apply(JSON.stringify({ apiVersion: "apps/v1", kind: "DaemonSet", metadata: { name: "web" }, spec: {
|
|
69
76
|
selector: { matchLabels: { app: "web" } }, template: { metadata: { labels: { app: "web" } }, spec: {
|
|
70
77
|
containers: [{ name: "web", image: "busybox:1.37", imagePullPolicy: "Never",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { networkInterfaces, tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { prepareK3sJoin } from "./k3s-join";
|
|
6
|
+
|
|
7
|
+
test("concurrent agents join the server's real numeric identity and retain the private token on restart", async () => {
|
|
8
|
+
const directory = await mkdtemp(join(tmpdir(), "k3s-join-"));
|
|
9
|
+
const children: ReturnType<typeof Bun.spawn>[] = [];
|
|
10
|
+
const start = (server: boolean) => {
|
|
11
|
+
const child = Bun.spawn(["sh", "-c", prepareK3sJoin(server, directory) + 'printf "%s" "$SERVER"'], {
|
|
12
|
+
stdout: "pipe", stderr: "pipe",
|
|
13
|
+
});
|
|
14
|
+
children.push(child);
|
|
15
|
+
return child;
|
|
16
|
+
};
|
|
17
|
+
const finish = async (child: ReturnType<typeof start>) => {
|
|
18
|
+
expect(await child.exited).toBe(0);
|
|
19
|
+
return new Response(child.stdout).text();
|
|
20
|
+
};
|
|
21
|
+
try {
|
|
22
|
+
const agents = [start(false), start(false)];
|
|
23
|
+
const address = await finish(start(true));
|
|
24
|
+
const localAddresses = Object.values(networkInterfaces()).flat().filter(a => a?.family === "IPv4").map(a => a!.address);
|
|
25
|
+
expect(localAddresses).toContain(address);
|
|
26
|
+
expect(await Promise.all(agents.map(finish))).toEqual([address, address]);
|
|
27
|
+
expect((await readFile(join(directory, "server"), "utf8")).trim()).toBe(address);
|
|
28
|
+
const token = await readFile(join(directory, "token"), "utf8");
|
|
29
|
+
expect(token.trim()).toMatch(/^[0-9a-f-]{36}$/);
|
|
30
|
+
expect((await stat(join(directory, "token"))).mode & 0o777).toBe(0o600);
|
|
31
|
+
expect(await finish(start(true))).toBe(address);
|
|
32
|
+
expect(await readFile(join(directory, "token"), "utf8")).toBe(token);
|
|
33
|
+
} finally {
|
|
34
|
+
for (const child of children) if (child.exitCode === null) child.kill();
|
|
35
|
+
await Promise.all(children.map(child => child.exited));
|
|
36
|
+
await rm(directory, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
}, 10_000);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const JOIN_DIRECTORY = "/etc/spectest-k3s-join";
|
|
2
|
+
|
|
3
|
+
// The join volume is private to one cluster and is captured with its network.
|
|
4
|
+
// Use the server's numeric address from the beginning: K3s otherwise demotes
|
|
5
|
+
// the DNS fallback after discovery and closes in-flight bootstrap requests.
|
|
6
|
+
export function prepareK3sJoin(server: boolean, directory = JOIN_DIRECTORY): string {
|
|
7
|
+
if (server) return (
|
|
8
|
+
'SERVER="$(ip -4 route get 1.1.1.1 | awk \'{for (i=1;i<NF;i++) if ($i=="src") {print $(i+1); exit}}\')"; ' +
|
|
9
|
+
'case "$SERVER" in ""|*[!0-9.]*) echo "spectest: missing server IPv4 address" >&2; exit 1;; esac; ' +
|
|
10
|
+
`printf '%s\\n' "$SERVER" > ${directory}/server.tmp && mv ${directory}/server.tmp ${directory}/server || exit 1; ` +
|
|
11
|
+
`if [ ! -s ${directory}/token ]; then (umask 077; cat /proc/sys/kernel/random/uuid > ${directory}/token) || exit 1; fi; `
|
|
12
|
+
);
|
|
13
|
+
return (
|
|
14
|
+
`while [ ! -s ${directory}/token ] || [ ! -s ${directory}/server ]; do sleep 0.1; done; ` +
|
|
15
|
+
`SERVER="$(cat ${directory}/server)"; `
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -22,7 +22,7 @@ test("one and multiple nodes use the same topology and preserve primary override
|
|
|
22
22
|
for (const name of ["cluster-node-2", "cluster-node-3"]) {
|
|
23
23
|
const node = three.services[name];
|
|
24
24
|
expect(node.command).toContain("exec k3s agent");
|
|
25
|
-
expect(node.command).toContain(
|
|
25
|
+
expect(node.command).toContain('--server="https://$SERVER:6443"');
|
|
26
26
|
expect(node.command).toContain("--kubelet-arg=cgroup-root=/spectest-$(hostname)");
|
|
27
27
|
expect(node.dependsOn).toBeUndefined(); // joins while the server boots
|
|
28
28
|
expect(node.imageBuilds).toHaveLength(0); // build once, import into every node
|
package/src/components/k3s.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isIP } from "node:net";
|
|
2
2
|
import { waitForK3sNodes } from "./k3s-nodes.js";
|
|
3
|
+
import { JOIN_DIRECTORY, prepareK3sJoin } from "./k3s-join.js";
|
|
3
4
|
import { installPodTrust } from "./k3s-trust.js";
|
|
4
5
|
import { k3sCoverage } from "./k3s-coverage.js";
|
|
5
6
|
import { waitForAll } from "../harness/concurrency.js";
|
|
@@ -189,7 +190,6 @@ export interface K3sOptions {
|
|
|
189
190
|
|
|
190
191
|
/** Port the in-cluster registry listens on (plain HTTP). */
|
|
191
192
|
const K3S_REGISTRY_PORT = 5000;
|
|
192
|
-
const JOIN_DIRECTORY = "/etc/spectest-k3s-join";
|
|
193
193
|
|
|
194
194
|
/** The cluster's own containerd root, inside the k3s container. */
|
|
195
195
|
const CLUSTER_STORE_TARGET = "/var/lib/rancher/k3s/agent/containerd";
|
|
@@ -2008,7 +2008,7 @@ function createK3sCluster(opts: K3sOptions, count: number) {
|
|
|
2008
2008
|
"--kubelet-arg=kubelet-cgroups=/spectest-$(hostname)/k3s",
|
|
2009
2009
|
"--kubelet-arg=runtime-cgroups=/spectest-$(hostname)/k3s",
|
|
2010
2010
|
`--token-file=${JOIN_DIRECTORY}/token`,
|
|
2011
|
-
...(server ? [] : [
|
|
2011
|
+
...(server ? [] : ['--server="https://$SERVER:6443"']),
|
|
2012
2012
|
// CoreDNS / pod DNS upstream. Without this, k3s sees only the
|
|
2013
2013
|
// loopback 127.0.0.11 (Docker's embedded DNS) in the container's
|
|
2014
2014
|
// /etc/resolv.conf, decides no usable nameserver exists, and writes a
|
|
@@ -2099,9 +2099,7 @@ function createK3sCluster(opts: K3sOptions, count: number) {
|
|
|
2099
2099
|
'sed "s/[^ ][^ ]*/+&/g" "/sys/fs/cgroup/$CG/cgroup.controllers" > "/sys/fs/cgroup/$CG/cgroup.subtree_control" || exit 1; ' +
|
|
2100
2100
|
'else for C in /sys/fs/cgroup/*; do [ ! -d "$C" ] || mkdir -p "$C/$CG/k3s" || exit 1; done; fi; ' +
|
|
2101
2101
|
storePrepareSh() +
|
|
2102
|
-
(server
|
|
2103
|
-
? `if [ ! -s ${JOIN_DIRECTORY}/token ]; then (umask 077; cat /proc/sys/kernel/random/uuid > ${JOIN_DIRECTORY}/token); fi; `
|
|
2104
|
-
: `while [ ! -s ${JOIN_DIRECTORY}/token ]; do sleep 0.1; done; `) +
|
|
2102
|
+
prepareK3sJoin(server) +
|
|
2105
2103
|
// Sticky and environment-local stores use overlayfs.
|
|
2106
2104
|
`exec ${nodeArgs} --snapshotter=$SNAP`;
|
|
2107
2105
|
// Plain /readyz probe. On a warm zot cache the cluster's images are
|
|
@@ -27,6 +27,12 @@ one backend snapshot, including closed byte ranges, exact ETag conditions,
|
|
|
27
27
|
version selection and SHA256 part receipts. Date-based copy conditions are
|
|
28
28
|
explicitly unsupported (501), rather than silently ignored.
|
|
29
29
|
|
|
30
|
+
Multipart completion stores its opaque ETag on the same in-memory object version
|
|
31
|
+
as the bytes. HEAD/GET, listings, copy conditions and atomic write conditions use
|
|
32
|
+
that tag; ordinary PUT and CopyObject retain their own payload-derived tags.
|
|
33
|
+
Versioned HEAD reads the requested version. The optional backend write port keeps
|
|
34
|
+
completion tags out of client metadata and avoids a separate post-write update.
|
|
35
|
+
|
|
30
36
|
Run `./test.sh` from this directory to exercise real HTTP requests against the
|
|
31
37
|
same patched sources used in the image, including corrupt retries, conditional
|
|
32
38
|
completion races, abort, metadata reconciliation and unchecksummed uploads.
|
|
@@ -147,39 +147,54 @@ func TestMultipartSHA256IntegrityAndReconciliation(t *testing.T) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
func TestMultipartConditionalCompletionIsAtomic(t *testing.T) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
statuses <- 0
|
|
164
|
-
return
|
|
150
|
+
for _, replace := range []bool{false, true} {
|
|
151
|
+
t.Run(fmt.Sprint(replace), func(t *testing.T) {
|
|
152
|
+
p := newProbe(t)
|
|
153
|
+
header, tag := "If-None-Match", "*"
|
|
154
|
+
if replace {
|
|
155
|
+
seed := p.start("same", true)
|
|
156
|
+
part := p.part(seed, 1, []byte("original"), true)
|
|
157
|
+
response := p.request("POST", seed, completion(part), nil, 200)
|
|
158
|
+
var receipt struct{ ETag string }
|
|
159
|
+
if err := xml.Unmarshal(response.body, &receipt); err != nil {
|
|
160
|
+
t.Fatal(err)
|
|
161
|
+
}
|
|
162
|
+
header, tag = "If-Match", receipt.ETag
|
|
165
163
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
statuses
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
164
|
+
paths := []string{p.start("same", true), p.start("same", true)}
|
|
165
|
+
parts := []string{p.part(paths[0], 1, []byte("first"), true), p.part(paths[1], 1, []byte("second"), true)}
|
|
166
|
+
statuses := make(chan int, 2)
|
|
167
|
+
var wg sync.WaitGroup
|
|
168
|
+
for i := range paths {
|
|
169
|
+
wg.Add(1)
|
|
170
|
+
go func(i int) {
|
|
171
|
+
defer wg.Done()
|
|
172
|
+
req, _ := http.NewRequest("POST", p.server.URL+paths[i], bytes.NewReader(completion(parts[i])))
|
|
173
|
+
req.Header.Set(header, tag)
|
|
174
|
+
res, err := p.server.Client().Do(req)
|
|
175
|
+
if err != nil {
|
|
176
|
+
statuses <- 0
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
defer res.Body.Close()
|
|
180
|
+
io.Copy(io.Discard, res.Body)
|
|
181
|
+
statuses <- res.StatusCode
|
|
182
|
+
}(i)
|
|
183
|
+
}
|
|
184
|
+
wg.Wait()
|
|
185
|
+
close(statuses)
|
|
186
|
+
counts := map[int]int{}
|
|
187
|
+
for s := range statuses {
|
|
188
|
+
counts[s]++
|
|
189
|
+
}
|
|
190
|
+
if counts[200] != 1 || counts[412] != 1 {
|
|
191
|
+
t.Fatalf("competing creates: %v", counts)
|
|
192
|
+
}
|
|
193
|
+
body := p.request("GET", "/uploads/same", nil, nil, 200).body
|
|
194
|
+
if string(body) != "first" && string(body) != "second" {
|
|
195
|
+
t.Fatalf("bad winner: %s", body)
|
|
196
|
+
}
|
|
197
|
+
})
|
|
183
198
|
}
|
|
184
199
|
}
|
|
185
200
|
|
|
@@ -280,3 +295,78 @@ func TestUploadPartCopyVersionChecksumAndAbort(t *testing.T) {
|
|
|
280
295
|
p.request("PUT", path+"&partNumber=1", nil, headers, 404)
|
|
281
296
|
p.request("DELETE", path, nil, nil, 204)
|
|
282
297
|
}
|
|
298
|
+
|
|
299
|
+
func TestMultipartSourceEntityTagSurvivesCopyVersionsAndOverwrite(t *testing.T) {
|
|
300
|
+
for _, count := range []int{1, 2} {
|
|
301
|
+
t.Run(fmt.Sprint(count), func(t *testing.T) {
|
|
302
|
+
p := newProbe(t)
|
|
303
|
+
p.request("PUT", "/uploads?versioning", []byte("<VersioningConfiguration><Status>Enabled</Status></VersioningConfiguration>"), nil, 200)
|
|
304
|
+
body := bytes.Repeat([]byte("a"), 6*1024*1024)
|
|
305
|
+
path := p.start("staged", false)
|
|
306
|
+
parts := []string{}
|
|
307
|
+
if count == 1 {
|
|
308
|
+
parts = append(parts, p.part(path, 1, body, false))
|
|
309
|
+
} else {
|
|
310
|
+
parts = append(parts, p.part(path, 1, body[:5*1024*1024], false), p.part(path, 2, body[5*1024*1024:], false))
|
|
311
|
+
}
|
|
312
|
+
complete := p.request("POST", path, completion(parts...), nil, 200)
|
|
313
|
+
var receipt struct{ ETag string }
|
|
314
|
+
if err := xml.Unmarshal(complete.body, &receipt); err != nil || !strings.Contains(receipt.ETag, fmt.Sprintf("-%d", count)) {
|
|
315
|
+
t.Fatalf("completion tag: %s: %v", complete.body, err)
|
|
316
|
+
}
|
|
317
|
+
version := complete.header.Get("x-amz-version-id")
|
|
318
|
+
if version == "" {
|
|
319
|
+
t.Fatal("missing completed version")
|
|
320
|
+
}
|
|
321
|
+
destination := p.start("copied-staging", false)
|
|
322
|
+
headers := map[string]string{"x-amz-copy-source": "/uploads/staged", "x-amz-copy-source-if-match": receipt.ETag, "x-amz-copy-source-range": fmt.Sprintf("bytes=0-%d", len(body)-1)}
|
|
323
|
+
copied := p.request("PUT", destination+"&partNumber=1", nil, headers, 200)
|
|
324
|
+
var part struct{ ETag string }
|
|
325
|
+
if err := xml.Unmarshal(copied.body, &part); err != nil {
|
|
326
|
+
t.Fatal(err)
|
|
327
|
+
}
|
|
328
|
+
for _, method := range []string{"HEAD", "GET"} {
|
|
329
|
+
got := p.request(method, "/uploads/staged", nil, nil, 200)
|
|
330
|
+
if got.header.Get("ETag") != receipt.ETag {
|
|
331
|
+
t.Fatalf("%s lost completion tag: %s", method, got.header.Get("ETag"))
|
|
332
|
+
}
|
|
333
|
+
p.request(method, "/uploads/staged", nil, map[string]string{"If-None-Match": receipt.ETag}, 304)
|
|
334
|
+
}
|
|
335
|
+
for _, query := range []string{"?list-type=2", "?versions"} {
|
|
336
|
+
got := p.request("GET", "/uploads"+query, nil, nil, 200)
|
|
337
|
+
var list struct {
|
|
338
|
+
Contents []struct{ ETag string }
|
|
339
|
+
Version []struct{ ETag string }
|
|
340
|
+
}
|
|
341
|
+
if err := xml.Unmarshal(got.body, &list); err != nil {
|
|
342
|
+
t.Fatal(err)
|
|
343
|
+
}
|
|
344
|
+
tags := []string{}
|
|
345
|
+
for _, row := range list.Contents {
|
|
346
|
+
tags = append(tags, row.ETag)
|
|
347
|
+
}
|
|
348
|
+
for _, row := range list.Version {
|
|
349
|
+
tags = append(tags, row.ETag)
|
|
350
|
+
}
|
|
351
|
+
if len(tags) != 1 || tags[0] != receipt.ETag {
|
|
352
|
+
t.Fatalf("list lost tag: %v", tags)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
put := p.request("PUT", "/uploads/staged", []byte("replacement"), map[string]string{"If-Match": receipt.ETag}, 200)
|
|
356
|
+
if put.header.Get("ETag") == receipt.ETag {
|
|
357
|
+
t.Fatal("normal overwrite inherited multipart tag")
|
|
358
|
+
}
|
|
359
|
+
p.request("PUT", destination+"&partNumber=1", nil, headers, 412)
|
|
360
|
+
headers["x-amz-copy-source"] += "?versionId=" + version
|
|
361
|
+
p.request("PUT", destination+"&partNumber=1", nil, headers, 200)
|
|
362
|
+
head := p.request("HEAD", "/uploads/staged?versionId="+version, nil, nil, 200)
|
|
363
|
+
if head.header.Get("ETag") != receipt.ETag {
|
|
364
|
+
t.Fatal("version lost its tag")
|
|
365
|
+
}
|
|
366
|
+
p.request("POST", destination, completion(fmt.Sprintf("<Part><PartNumber>1</PartNumber><ETag>%s</ETag></Part>", part.ETag)), nil, 200)
|
|
367
|
+
if got := p.request("GET", "/uploads/copied-staging", nil, nil, 200); !bytes.Equal(got.body, body) {
|
|
368
|
+
t.Fatal("copied multipart staging bytes changed")
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -350,3 +350,199 @@
|
|
|
350
350
|
+ ChecksumSHA256 string `xml:"ChecksumSHA256"`
|
|
351
351
|
+ }{ETag: etag, LastModified: NewContentTime(g.timeSource.Now()), ChecksumSHA256: base64.StdEncoding.EncodeToString(digest.Sum(nil))})
|
|
352
352
|
+}
|
|
353
|
+
--- a/vendor/github.com/johannesboyne/gofakes3/backend.go
|
|
354
|
+
+++ b/vendor/github.com/johannesboyne/gofakes3/backend.go
|
|
355
|
+
@@ -23,7 +23,9 @@
|
|
356
|
+
Size int64
|
|
357
|
+
Contents io.ReadCloser
|
|
358
|
+
Hash []byte
|
|
359
|
+
- Range *ObjectRange
|
|
360
|
+
+ // EntityTag is the stored opaque tag; Hash remains the payload MD5.
|
|
361
|
+
+ EntityTag string
|
|
362
|
+
+ Range *ObjectRange
|
|
363
|
+
|
|
364
|
+
// VersionID will be empty if bucket versioning has not been enabled.
|
|
365
|
+
VersionID VersionID
|
|
366
|
+
@@ -31,6 +33,21 @@
|
|
367
|
+
// If versioning is enabled for the bucket, this is true if this object version
|
|
368
|
+
// is a delete marker.
|
|
369
|
+
IsDeleteMarker bool
|
|
370
|
+
+}
|
|
371
|
+
+
|
|
372
|
+
+// ETag returns the version-owned tag, retaining compatibility with backends
|
|
373
|
+
+// that only supply payload hashes.
|
|
374
|
+
+func (object *Object) ETag() string {
|
|
375
|
+
+ if object.EntityTag != "" {
|
|
376
|
+
+ return object.EntityTag
|
|
377
|
+
+ }
|
|
378
|
+
+ return FormatETag(object.Hash)
|
|
379
|
+
+}
|
|
380
|
+
+
|
|
381
|
+
+// MultipartObjectBackend atomically stores completion bytes, metadata and ETag
|
|
382
|
+
+// under the same conditional-write lock. The tag never enters client metadata.
|
|
383
|
+
+type MultipartObjectBackend interface {
|
|
384
|
+
+ PutMultipartObject(bucket, key string, metadata map[string]string, input io.Reader, size int64, tag string, conditions *PutConditions) (PutObjectResult, error)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
type ObjectList struct {
|
|
388
|
+
@@ -150,7 +167,8 @@
|
|
389
|
+
|
|
390
|
+
// Hash is the MD5 hash of the object content (used for ETag comparison)
|
|
391
|
+
// Only required if Exists is true
|
|
392
|
+
- Hash []byte
|
|
393
|
+
+ Hash []byte
|
|
394
|
+
+ EntityTag string
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ObjectInfo is a deprecated alias for ConditionalObjectInfo.
|
|
398
|
+
@@ -182,7 +200,10 @@
|
|
399
|
+
if len(expectedETag) >= 2 && expectedETag[0] == '"' && expectedETag[len(expectedETag)-1] == '"' {
|
|
400
|
+
expectedETag = expectedETag[1 : len(expectedETag)-1]
|
|
401
|
+
}
|
|
402
|
+
- actualETag := hex.EncodeToString(objectInfo.Hash)
|
|
403
|
+
+ actualETag := strings.Trim(objectInfo.EntityTag, "\"")
|
|
404
|
+
+ if actualETag == "" {
|
|
405
|
+
+ actualETag = hex.EncodeToString(objectInfo.Hash)
|
|
406
|
+
+ }
|
|
407
|
+
if expectedETag != actualETag {
|
|
408
|
+
return ErrorMessage(ErrPreconditionFailed, "The ETag does not match")
|
|
409
|
+
}
|
|
410
|
+
--- a/vendor/github.com/johannesboyne/gofakes3/uploader.go
|
|
411
|
+
+++ b/vendor/github.com/johannesboyne/gofakes3/uploader.go
|
|
412
|
+
@@ -496,7 +496,12 @@
|
|
413
|
+
input.ChecksumSHA256 = ""
|
|
414
|
+
input.ChecksumType = ""
|
|
415
|
+
}
|
|
416
|
+
- result, err := u.storage.PutObject(bucket, object, mpu.Meta, bytes.NewReader(body), int64(len(body)), input.Conditions)
|
|
417
|
+
+ var result PutObjectResult
|
|
418
|
+
+ if backend, ok := u.storage.(MultipartObjectBackend); ok {
|
|
419
|
+
+ result, err = backend.PutMultipartObject(bucket, object, mpu.Meta, bytes.NewReader(body), int64(len(body)), etag, input.Conditions)
|
|
420
|
+
+ } else {
|
|
421
|
+
+ result, err = u.storage.PutObject(bucket, object, mpu.Meta, bytes.NewReader(body), int64(len(body)), input.Conditions)
|
|
422
|
+
+ }
|
|
423
|
+
if err != nil {
|
|
424
|
+
return "", "", err
|
|
425
|
+
}
|
|
426
|
+
--- a/vendor/github.com/johannesboyne/gofakes3/gofakes3.go
|
|
427
|
+
+++ b/vendor/github.com/johannesboyne/gofakes3/gofakes3.go
|
|
428
|
+
@@ -535,7 +535,7 @@
|
|
429
|
+
w.Header().Set("x-amz-version-id", string(obj.VersionID))
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
- etag := `"` + hex.EncodeToString(obj.Hash) + `"`
|
|
433
|
+
+ etag := obj.ETag()
|
|
434
|
+
w.Header().Set("ETag", etag)
|
|
435
|
+
|
|
436
|
+
if r.Header.Get("If-None-Match") == etag {
|
|
437
|
+
@@ -576,7 +576,16 @@
|
|
438
|
+
return err
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
- obj, err := g.storage.HeadObject(bucket, object)
|
|
442
|
+
+ var obj *Object
|
|
443
|
+
+ var err error
|
|
444
|
+
+ if versionID == "" {
|
|
445
|
+
+ obj, err = g.storage.HeadObject(bucket, object)
|
|
446
|
+
+ } else {
|
|
447
|
+
+ if g.versioned == nil {
|
|
448
|
+
+ return ErrNotImplemented
|
|
449
|
+
+ }
|
|
450
|
+
+ obj, err = g.versioned.HeadObjectVersion(bucket, object, versionID)
|
|
451
|
+
+ }
|
|
452
|
+
if err != nil {
|
|
453
|
+
return err
|
|
454
|
+
}
|
|
455
|
+
--- a/vendor/github.com/johannesboyne/gofakes3/upload_part_copy.go
|
|
456
|
+
+++ b/vendor/github.com/johannesboyne/gofakes3/upload_part_copy.go
|
|
457
|
+
@@ -56,7 +56,7 @@
|
|
458
|
+
if src.IsDeleteMarker {
|
|
459
|
+
return ErrInvalidArgument
|
|
460
|
+
}
|
|
461
|
+
- tag := FormatETag(src.Hash)
|
|
462
|
+
+ tag := src.ETag()
|
|
463
|
+
if expected := r.Header.Get("x-amz-copy-source-if-match"); expected != "" && expected != tag {
|
|
464
|
+
return ErrPreconditionFailed
|
|
465
|
+
}
|
|
466
|
+
--- a/vendor/github.com/johannesboyne/gofakes3/backend/s3mem/backend.go
|
|
467
|
+
+++ b/vendor/github.com/johannesboyne/gofakes3/backend/s3mem/backend.go
|
|
468
|
+
@@ -119,7 +119,7 @@
|
|
469
|
+
response.Add(&gofakes3.Content{
|
|
470
|
+
Key: item.data.name,
|
|
471
|
+
LastModified: gofakes3.NewContentTime(item.data.lastModified),
|
|
472
|
+
- ETag: gofakes3.FormatETag(item.data.hash),
|
|
473
|
+
+ ETag: item.data.entityTag,
|
|
474
|
+
Size: int64(len(item.data.body)),
|
|
475
|
+
})
|
|
476
|
+
}
|
|
477
|
+
@@ -240,6 +240,16 @@
|
|
478
|
+
size int64,
|
|
479
|
+
conditions *gofakes3.PutConditions,
|
|
480
|
+
) (result gofakes3.PutObjectResult, err error) {
|
|
481
|
+
+ return db.putObject(bucketName, objectName, meta, input, size, "", conditions)
|
|
482
|
+
+}
|
|
483
|
+
+
|
|
484
|
+
+// PutMultipartObject retains the completion tag in the same object version as
|
|
485
|
+
+// its bytes and uses the ordinary atomic conditional-write admission.
|
|
486
|
+
+func (db *Backend) PutMultipartObject(bucketName, objectName string, meta map[string]string, input io.Reader, size int64, tag string, conditions *gofakes3.PutConditions) (gofakes3.PutObjectResult, error) {
|
|
487
|
+
+ return db.putObject(bucketName, objectName, meta, input, size, tag, conditions)
|
|
488
|
+
+}
|
|
489
|
+
+
|
|
490
|
+
+func (db *Backend) putObject(bucketName, objectName string, meta map[string]string, input io.Reader, size int64, tag string, conditions *gofakes3.PutConditions) (result gofakes3.PutObjectResult, err error) {
|
|
491
|
+
// No need to lock the backend while we read the data into memory; it holds
|
|
492
|
+
// the write lock open unnecessarily, and could be blocked for an unreasonably
|
|
493
|
+
// long time by a connection timing out:
|
|
494
|
+
@@ -272,11 +282,15 @@
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
hash := md5.Sum(bts)
|
|
498
|
+
+ if tag == "" {
|
|
499
|
+
+ tag = gofakes3.FormatETag(hash[:])
|
|
500
|
+
+ }
|
|
501
|
+
|
|
502
|
+
item := &bucketData{
|
|
503
|
+
name: objectName,
|
|
504
|
+
body: bts,
|
|
505
|
+
hash: hash[:],
|
|
506
|
+
+ entityTag: tag,
|
|
507
|
+
metadata: meta,
|
|
508
|
+
lastModified: db.timeSource.Now(),
|
|
509
|
+
}
|
|
510
|
+
@@ -557,7 +571,7 @@
|
|
511
|
+
IsLatest: version == object.data,
|
|
512
|
+
LastModified: gofakes3.NewContentTime(version.lastModified),
|
|
513
|
+
Size: int64(len(version.body)),
|
|
514
|
+
- ETag: gofakes3.FormatETag(version.hash),
|
|
515
|
+
+ ETag: version.entityTag,
|
|
516
|
+
}
|
|
517
|
+
if bucket.versioning != gofakes3.VersioningNone { // S300005
|
|
518
|
+
resultVer.VersionID = version.versionID
|
|
519
|
+
@@ -587,8 +601,9 @@
|
|
520
|
+
return &gofakes3.ConditionalObjectInfo{Exists: false}, nil
|
|
521
|
+
}
|
|
522
|
+
return &gofakes3.ConditionalObjectInfo{
|
|
523
|
+
- Exists: true,
|
|
524
|
+
- Hash: existing.data.hash,
|
|
525
|
+
+ Exists: true,
|
|
526
|
+
+ Hash: existing.data.hash,
|
|
527
|
+
+ EntityTag: existing.data.entityTag,
|
|
528
|
+
}, nil
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
--- a/vendor/github.com/johannesboyne/gofakes3/backend/s3mem/bucket.go
|
|
532
|
+
+++ b/vendor/github.com/johannesboyne/gofakes3/backend/s3mem/bucket.go
|
|
533
|
+
@@ -118,6 +118,7 @@
|
|
534
|
+
deleteMarker bool
|
|
535
|
+
body []byte
|
|
536
|
+
hash []byte
|
|
537
|
+
+ entityTag string
|
|
538
|
+
metadata map[string]string
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
@@ -150,6 +151,7 @@
|
|
542
|
+
return &gofakes3.Object{
|
|
543
|
+
Name: bi.name,
|
|
544
|
+
Hash: bi.hash,
|
|
545
|
+
+ EntityTag: bi.entityTag,
|
|
546
|
+
Metadata: bi.metadata,
|
|
547
|
+
Size: sz,
|
|
548
|
+
Range: rnge,
|