Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/cluster.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ curl -s -H "Authorization: Bearer $TOKEN" http://node-a:7777/v1/info | jq .
```json
{
"pools": [
{"key": {"template": "base:24.04", "net": "none", "size": "small", "engine": "ch"},
{"key": {"template": "base:24.04", "net": "none", "size": "small"},
"warm": 4, "refilling": 0, "target": 4, "golden": true}
],
"claimed": 2,
Expand Down
5 changes: 2 additions & 3 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ sandboxd reads one JSON file (`-config`, default
| `archive_after_seconds` | 0 (off) | tier below hibernation: a hibernated claim idle this long is checkpointed to the store and its local VM dropped, freeing the node entirely; the next call restores it transparently (a checkpoint restore's latency). Requires `idle_hibernate_seconds > 0` and must exceed it. Node-wide for unpooled keys; per-pool overrides for that pool |
| `archive_delete_after_seconds` | 0 (keep) | purge an archived claim's store checkpoint this long after it was archived, reclaiming storage; the claim is then gone for good. Same node-wide/per-pool split |
| `mesh` | unset | join a cluster ([Clusters](cluster.md)); unset = single node |
| `pools[]` | — | warm pools, keyed by `(template, net, size, engine)`. `warm` defaults to 4; `net` is `none` or `egress`; `size` is a tier, below; `engine` is `ch` (default) or `fc` to cold-boot that key under Firecracker. Retune online without a restart via [`PUT /v1/pools`](sandboxd-api.md#put-v1pools) — omitted pools drain. This is the **first-boot seed**: once a node takes a `PUT /v1/pools`, the applied set persists to `<data_dir>/pools.json` and overrides this section on every later boot (a startup log notes it); delete `pools.json` to return to config-owned pools. Egress stays config-owned either way. See [state ownership](cluster.md#state-ownership) |
| `pools[]` | — | warm pools, keyed by `(template, net, size)`. `warm` defaults to 4; `net` is `none` or `egress`; `size` is a tier, below. Retune online without a restart via [`PUT /v1/pools`](sandboxd-api.md#put-v1pools) — omitted pools drain. This is the **first-boot seed**: once a node takes a `PUT /v1/pools`, the applied set persists to `<data_dir>/pools.json` and overrides this section on every later boot (a startup log notes it); delete `pools.json` to return to config-owned pools. Egress stays config-owned either way. See [state ownership](cluster.md#state-ownership) |

Size tiers (free-form CPU/memory is deliberately not accepted — it would
fragment the warm pools):
Expand Down Expand Up @@ -157,8 +157,7 @@ mounts the device before finalizing the claim: `mode: "ro"` (the default)
attaches and mounts read-only; `mode: "rw"` requires the catalog entry's
`writable: true` and attaches and mounts read-write. Setup failure destroys
the VM without quiescing — the claim was never handed out, so no workload
write happened — and a popped warm VM is refilled normally. Firecracker
volume claims are rejected.
write happened — and a popped warm VM is refilled normally.

A multi-volume claim brings every volume up concurrently: each volume's own
marker→attach→mount order is preserved, but cocoon serializes the hypervisor
Expand Down
15 changes: 6 additions & 9 deletions docs/sandboxd-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,8 @@ Auth: `Authorization: Bearer <api_token>` (when configured).
"require_promoted": false}
```

- `net` defaults to `none`, `size` to `small`, `engine` to `ch`. The pool key
is `(template, net, size, engine)`; `engine: "fc"` cold-boots that key under
Firecracker, and clones inherit the hypervisor pinned in the golden's
snapshot
- `net` defaults to `none`, `size` to `small`; the pool key is
`(template, net, size)`
- `ttl_seconds` 0 means the server default (5 minutes); capped at 24h. The
owning node reaps the sandbox after the TTL even if the client vanishes
- `claim_ref` is an optional opaque caller reference echoed by the scoped
Expand All @@ -48,8 +46,7 @@ Auth: `Authorization: Bearer <api_token>` (when configured).
defaults to `/volumes/<name>`; a custom value must be absolute and clean,
outside the guest OS tree, unique, and non-nesting within the request.
`mode` is `"ro"` (default, omitted) or `"rw"`; `"rw"` requires the catalog
entry's `writable: true` (see [deploy](deploy.md#dataset-volumes)). Volumes
require Cloud Hypervisor
entry's `writable: true` (see [deploy](deploy.md#dataset-volumes))
- `volumes_attach_only` (default `false`) attaches every requested volume
without mounting it, handing the whole mount contract to the workload. It
requires at least one volume, and rejects any entry carrying a `mount` —
Expand Down Expand Up @@ -175,7 +172,7 @@ Errors: 400 unknown template axis, invalid/duplicate volumes,
`volumes_attach_only` with no volumes or with an entry carrying a `mount`,
`mode: "rw"`
against a non-writable entry, or a volume that is unknown or forbidden (the
latter two are deliberately indistinguishable), Firecracker with volumes, or
latter two are deliberately indistinguishable), or
bad body; 401 bad api token; 409 egress requested on a node without an egress
attachment, a writable name already claimed in a conflicting mode (volume
busy — a live writer excludes every other claim for that name, live readers
Expand Down Expand Up @@ -280,7 +277,7 @@ node (name-based calls route via gossip); a shared checkpoint store makes
every node resolve it. Under exactly this key:

```json
{"key": {"template": "myproj:v1", "net": "none", "size": "small", "engine": "ch"},
{"key": {"template": "myproj:v1", "net": "none", "size": "small"},
"content_digest": "sha256:…"}
```

Expand Down Expand Up @@ -522,7 +519,7 @@ Auth: root only (tenant tokens get 403). Node pools, claim count, and mesh
peers:

```json
{"pools": [{"key": {"template": "base:24.04", "net": "none", "size": "small", "engine": "ch"},
{"pools": [{"key": {"template": "base:24.04", "net": "none", "size": "small"},
"warm": 4, "refilling": 0, "target": 4, "golden": true}],
"claimed": 2,
"hibernated": 1,
Expand Down
5 changes: 1 addition & 4 deletions e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
sandbox "github.com/cocoonstack/sandbox/sdk/go"
)

var testKey = types.PoolKey{Template: "rt:24.04", Net: types.NetNone, Size: types.SizeSmall, Engine: types.EngineCH}
var testKey = types.PoolKey{Template: "rt:24.04", Net: types.NetNone, Size: types.SizeSmall}

func TestEndToEnd(t *testing.T) {
stack := startStack(t, "node-token", config.PoolSpec{PoolKey: testKey, Warm: 1})
Expand Down Expand Up @@ -446,9 +446,6 @@ func TestClaimRefRoundTrip(t *testing.T) {
if list[i].ClaimRef != "ns/workload" {
t.Errorf("claim_ref %q, want ns/workload", list[i].ClaimRef)
}
if list[i].Key.Engine != sandbox.EngineCH {
t.Errorf("engine %q, want the defaulted ch — the SDK key must carry the axis", list[i].Key.Engine)
}
}

// TestAttachOnlyVolumeEndToEnd drives one attach-only writable claim through
Expand Down
5 changes: 0 additions & 5 deletions sandboxd/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,11 +358,6 @@ func (e *Engine) restoreArgs() []string {
func (e *Engine) runColdArgs(name string, key types.PoolKey) []string {
spec, _ := key.Size.Spec()
args := []string{"vm", "run", argName, name, argOutput, formatJSON, "--cpu", strconv.Itoa(spec.CPU), "--memory", spec.Memory, e.directIOArg()}
if key.Engine == types.EngineFC {
// Firecracker is a per-pool cold-boot choice; clones inherit the
// hypervisor from the golden's pinned snapshot, so only RunCold flags it.
args = append(args, "--fc")
}
args = append(args, e.netArgs(name, key, true)...)
return append(args, key.Template)
}
Expand Down
4 changes: 2 additions & 2 deletions sandboxd/pool/claim.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func (m *Manager) ClaimWarm(ctx context.Context, key types.PoolKey, ttl time.Dur
if err := m.validate(key); err != nil {
return nil, err
}
volumeSpecs, err := m.resolveVolumes(ctx, key, tenant, volumes)
volumeSpecs, err := m.resolveVolumes(ctx, tenant, volumes)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -506,7 +506,7 @@ func (m *Manager) claimProvision(ctx context.Context, key types.PoolKey, ttl tim
if err := m.validate(key); err != nil {
return nil, err
}
volumeSpecs, err := m.resolveVolumes(ctx, key, tenant, volumes)
volumeSpecs, err := m.resolveVolumes(ctx, tenant, volumes)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion sandboxd/pool/egress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
)

var (
egKey = types.PoolKey{Template: "rt:24.04", Net: types.NetEgress, Size: types.SizeSmall, Engine: types.EngineCH}
egKey = types.PoolKey{Template: "rt:24.04", Net: types.NetEgress, Size: types.SizeSmall}
egPolicy = &egress.Policy{Allow: []egress.Rule{{Host: "example.com", Secret: "gh"}}}
)

Expand Down
2 changes: 1 addition & 1 deletion sandboxd/pool/intercept_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"github.com/cocoonstack/sandbox/sandboxd/types"
)

var interceptKey = types.PoolKey{Template: "rt:24.04", Net: types.NetNone, Size: types.SizeSmall, Engine: types.EngineCH}
var interceptKey = types.PoolKey{Template: "rt:24.04", Net: types.NetNone, Size: types.SizeSmall}

func TestGoldenBuildInstallsCAForInterceptPool(t *testing.T) {
eng := newFakeEngine()
Expand Down
2 changes: 1 addition & 1 deletion sandboxd/pool/pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import (
"github.com/cocoonstack/sandbox/sandboxd/types"
)

var testKey = types.PoolKey{Template: "rt:24.04", Net: types.NetNone, Size: types.SizeSmall, Engine: types.EngineCH}
var testKey = types.PoolKey{Template: "rt:24.04", Net: types.NetNone, Size: types.SizeSmall}

func TestClaimWarmHitTransfersOwnership(t *testing.T) {
eng := newFakeEngine()
Expand Down
4 changes: 2 additions & 2 deletions sandboxd/pool/poolstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
)

var (
seedKey = types.PoolKey{Template: "seed:24.04", Net: types.NetNone, Size: types.SizeSmall, Engine: types.EngineCH}
apiKey = types.PoolKey{Template: "api:24.04", Net: types.NetNone, Size: types.SizeSmall, Engine: types.EngineCH}
seedKey = types.PoolKey{Template: "seed:24.04", Net: types.NetNone, Size: types.SizeSmall}
apiKey = types.PoolKey{Template: "api:24.04", Net: types.NetNone, Size: types.SizeSmall}
)

func TestPersistedPoolsSurviveRestart(t *testing.T) {
Expand Down
8 changes: 4 additions & 4 deletions sandboxd/pool/promote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestPromoteThenClaimClonesFromTemplate(t *testing.T) {
if len(eng.snapSaves) != 1 || !slices.Contains(eng.snapRemoves, eng.snapSaves[0]) {
t.Errorf("snapSaves=%v snapRemoves=%v, want one transient snapshot dropped", eng.snapSaves, eng.snapRemoves)
}
key := types.PoolKey{Template: "tpl:x", Net: parent.Key.Net, Size: parent.Key.Size, Engine: parent.Key.Engine}
key := types.PoolKey{Template: "tpl:x", Net: parent.Key.Net, Size: parent.Key.Size}
if gotKey != key {
t.Errorf("returned key %+v, want %+v (the parent's axes)", gotKey, key)
}
Expand Down Expand Up @@ -146,12 +146,12 @@ func TestDeleteTemplate(t *testing.T) {
if _, _, err := m.Promote(t.Context(), parent.ID, Cred{Token: parent.Token}, "tpl:del", ""); err != nil {
t.Fatalf("Promote: %v", err)
}
key := types.PoolKey{Template: "tpl:del", Net: testKey.Net, Size: testKey.Size, Engine: testKey.Engine}
key := types.PoolKey{Template: "tpl:del", Net: testKey.Net, Size: testKey.Size}

if err := m.DeleteTemplate(t.Context(), testKey, ""); !errors.Is(err, ErrPooledTemplate) {
t.Errorf("pooled delete: %v, want ErrPooledTemplate", err)
}
if err := m.DeleteTemplate(t.Context(), types.PoolKey{Template: "nope", Net: testKey.Net, Size: testKey.Size, Engine: testKey.Engine}, ""); !errors.Is(err, ErrUnknownTemplate) {
if err := m.DeleteTemplate(t.Context(), types.PoolKey{Template: "nope", Net: testKey.Net, Size: testKey.Size}, ""); !errors.Is(err, ErrUnknownTemplate) {
t.Errorf("unknown delete: %v, want ErrUnknownTemplate", err)
}
if err := m.DeleteTemplate(t.Context(), key, ""); err != nil {
Expand Down Expand Up @@ -276,7 +276,7 @@ func TestPromoteFailsClosedOnMetaError(t *testing.T) {
if _, _, err := m.Promote(t.Context(), a.ID, Cred{Token: a.Token}, "shared:v1", "acme"); err != nil {
t.Fatalf("promote: %v", err)
}
key := types.PoolKey{Template: "shared:v1", Net: testKey.Net, Size: testKey.Size, Engine: testKey.Engine}
key := types.PoolKey{Template: "shared:v1", Net: testKey.Net, Size: testKey.Size}
meta := filepath.Join(m.dataDir, "checkpoints", store.TemplateID(key.Hash()), store.MetaFile)
if err := os.Chmod(meta, 0o000); err != nil {
t.Fatalf("chmod: %v", err)
Expand Down
2 changes: 1 addition & 1 deletion sandboxd/pool/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (m *Manager) Promote(ctx context.Context, id string, cred Cred, template, t
if !sb.Key.Capturable() {
return types.PoolKey{}, "", ErrNoEgressFork
}
key := types.PoolKey{Template: template, Net: sb.Key.Net, Size: sb.Key.Size, Engine: sb.Key.Engine}
key := types.PoolKey{Template: template, Net: sb.Key.Net, Size: sb.Key.Size}
if m.pooledHash(key.Hash()) {
// A configured pool owns this key — promoting over it would
// silently change what refills produce.
Expand Down
5 changes: 1 addition & 4 deletions sandboxd/pool/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ func (m *Manager) VolumePlacement(key types.PoolKey, tenant string, names []stri
return local, nil
}

func (m *Manager) resolveVolumes(ctx context.Context, key types.PoolKey, tenant string, requested []types.Volume) ([]resolvedVolume, error) {
func (m *Manager) resolveVolumes(ctx context.Context, tenant string, requested []types.Volume) ([]resolvedVolume, error) {
if len(requested) == 0 {
return nil, nil
}
Expand All @@ -145,9 +145,6 @@ func (m *Manager) resolveVolumes(ctx context.Context, key types.PoolKey, tenant
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrBadVolume, err)
}
if key.Engine != types.EngineCH {
return nil, fmt.Errorf("%w: volumes require engine ch", ErrBadVolume)
}
resolved := make([]resolvedVolume, 0, len(applied))
for _, volume := range applied {
entry, ok := m.volumes[volume.Name]
Expand Down
4 changes: 2 additions & 2 deletions sandboxd/pool/volume_rw_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func TestConfirmVolumesCleanCatchesMarkerAfterAdmission(t *testing.T) {

// The reader resolves a clean image; a writable claim then fails between
// that resolve and admission, leaving its marker but no hold behind.
resolved, err := m.resolveVolumes(t.Context(), testKey, "", readOnly)
resolved, err := m.resolveVolumes(t.Context(), "", readOnly)
if err != nil {
t.Fatalf("resolveVolumes: %v", err)
}
Expand All @@ -166,7 +166,7 @@ func TestConfirmVolumesCleanCatchesMarkerAfterAdmission(t *testing.T) {
}
m.unreserveVolumes(appliedVolumes(resolved))

writable, err := m.resolveVolumes(t.Context(), testKey, "", []types.Volume{{Name: "scratch", Mode: types.VolumeModeRW}})
writable, err := m.resolveVolumes(t.Context(), "", []types.Volume{{Name: "scratch", Mode: types.VolumeModeRW}})
if err != nil {
t.Fatalf("resolveVolumes writable: %v", err)
}
Expand Down
3 changes: 1 addition & 2 deletions sandboxd/pool/volume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,6 @@ func TestClaimProvisionRejectsInvalidVolumesBeforeProvision(t *testing.T) {
{"too many", testKey, nil, tooMany},
{"unknown", testKey, []config.VolumeSpec{{Name: "data", Path: path}}, []types.Volume{{Name: "other"}}},
{"invalid mount", testKey, []config.VolumeSpec{{Name: "data", Path: path}}, []types.Volume{{Name: "data", Mount: "relative"}}},
{"firecracker", types.PoolKey{Template: "rt:24.04", Net: types.NetNone, Size: types.SizeSmall, Engine: types.EngineFC}, []config.VolumeSpec{{Name: "data", Path: path}}, []types.Volume{{Name: "data"}}},
} {
t.Run(tt.name, func(t *testing.T) {
eng := newFakeEngine()
Expand Down Expand Up @@ -472,7 +471,7 @@ func TestVolumePlacementChecksAccessAndLocalAvailability(t *testing.T) {
if local, err := m.VolumePlacement(testKey, "", []string{"peer-only"}); err != nil || local {
t.Errorf("root peer-only placement=(%v, %v), want false, nil", local, err)
}
badKey := types.PoolKey{Template: "rt:24.04", Net: "lan", Size: types.SizeSmall, Engine: types.EngineCH}
badKey := types.PoolKey{Template: "rt:24.04", Net: "lan", Size: types.SizeSmall}
if _, err := m.VolumePlacement(badKey, "", []string{"local"}); !errors.Is(err, ErrBadKey) {
t.Errorf("invalid key error=%v, want ErrBadKey", err)
}
Expand Down
4 changes: 2 additions & 2 deletions sandboxd/server/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ func (s *Server) handleMetrics(w http.ResponseWriter, _ *http.Request) {

metric("pool_warm", "gauge", "claim-ready VMs per pool")
for _, p := range pools {
_, _ = fmt.Fprintf(w, "sandboxd_pool_warm{template=%q,net=%q,size=%q,engine=%q} %d\n", p.Key.Template, p.Key.Net, p.Key.Size, p.Key.Engine, p.Warm)
_, _ = fmt.Fprintf(w, "sandboxd_pool_warm{template=%q,net=%q,size=%q} %d\n", p.Key.Template, p.Key.Net, p.Key.Size, p.Warm)
}
metric("pool_target", "gauge", "warm watermark per pool")
for _, p := range pools {
_, _ = fmt.Fprintf(w, "sandboxd_pool_target{template=%q,net=%q,size=%q,engine=%q} %d\n", p.Key.Template, p.Key.Net, p.Key.Size, p.Key.Engine, p.Target)
_, _ = fmt.Fprintf(w, "sandboxd_pool_target{template=%q,net=%q,size=%q} %d\n", p.Key.Template, p.Key.Net, p.Key.Size, p.Target)
}

if s.placer != nil {
Expand Down
3 changes: 0 additions & 3 deletions sandboxd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,6 @@ func (s *Server) handleClaim(w http.ResponseWriter, r *http.Request) {

func (s *Server) handleVolumeClaim(w http.ResponseWriter, r *http.Request, req types.ClaimRequest, key types.PoolKey, hash, tenant string) {
volumes, err := types.ValidateVolumes(req.Volumes, req.VolumesAttachOnly)
if err == nil && key.Engine != types.EngineCH {
err = errors.New("volumes require engine ch")
}
if err != nil {
writeErr(w, http.StatusBadRequest, fmt.Errorf("%w: %v", pool.ErrBadVolume, err).Error())
return
Expand Down
5 changes: 2 additions & 3 deletions sandboxd/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func TestClaimHappyPath(t *testing.T) {
if cr.TemplateDigest != "sha256:claim-digest" {
t.Errorf("template digest %q, want sha256:claim-digest", cr.TemplateDigest)
}
want := types.PoolKey{Template: "rt:24.04", Net: types.NetNone, Size: types.SizeSmall, Engine: types.EngineCH}
want := types.PoolKey{Template: "rt:24.04", Net: types.NetNone, Size: types.SizeSmall}
if gotKey != want {
t.Errorf("key %+v, want defaults %+v", gotKey, want)
}
Expand Down Expand Up @@ -738,7 +738,7 @@ func TestPromoteAndDeleteTemplateFlow(t *testing.T) {
if got := del("Bearer sekret", "template=tpl:x&net=none&size=small"); got != http.StatusNoContent {
t.Errorf("delete status %d, want 204", got)
}
want := types.PoolKey{Template: "tpl:x", Net: types.NetNone, Size: types.SizeSmall, Engine: types.EngineCH}
want := types.PoolKey{Template: "tpl:x", Net: types.NetNone, Size: types.SizeSmall}
if gotKey != want {
t.Errorf("delete key %+v, want %+v (claim defaults applied)", gotKey, want)
}
Expand Down Expand Up @@ -1394,7 +1394,6 @@ func TestVolumeClaimRejectsShapeBeforePlacement(t *testing.T) {
`{"template":"rt:24.04","volumes":["data"]}`,
`{"template":"rt:24.04","volumes":[{"name":"data"},{"name":"data"}]}`,
`{"template":"rt:24.04","volumes":[{"name":"cocoon-data"}]}`,
`{"template":"rt:24.04","engine":"fc","volumes":[{"name":"data"}]}`,
`{"template":"rt:24.04","volumes":[{"name":"data","mount":"relative"}]}`,
`{"template":"rt:24.04","volumes":[{"name":"data","mount":"/datasets"},{"name":"other","mount":"/datasets/nested"}]}`,
`{"template":"rt:24.04","volumes":[{"name":"a"},{"name":"b"},{"name":"c"},{"name":"d"},{"name":"e"},{"name":"f"},{"name":"g"},{"name":"h"},{"name":"i"}]}`,
Expand Down
3 changes: 1 addition & 2 deletions sandboxd/types/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ type ClaimRequest struct {
Template string `json:"template"`
Net NetShape `json:"net,omitempty"`
Size Size `json:"size,omitempty"`
Engine Engine `json:"engine,omitempty"`
Volumes []Volume `json:"volumes,omitempty"`
// VolumesAttachOnly attaches every requested volume without mounting it:
// the workload finds the device by its serial and owns the mount contract.
Expand All @@ -40,7 +39,7 @@ type ClaimRequest struct {

// Key resolves the requested pool key with the wire defaults filled.
func (r ClaimRequest) Key() PoolKey {
return PoolKey{Template: r.Template, Net: r.Net, Size: r.Size, Engine: r.Engine}.Defaulted()
return PoolKey{Template: r.Template, Net: r.Net, Size: r.Size}.Defaulted()
}

// ClaimResponse is the wire reply of POST /v1/claim. A successful claim
Expand Down
Loading