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
4 changes: 4 additions & 0 deletions cmd/sandbox-operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/metrics"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"

Expand Down Expand Up @@ -228,6 +229,9 @@ func (o *options) run() error {
return fmt.Errorf("start manager: %w", err)
}

if err := asmetrics.Register(metrics.Registry); err != nil {
return err
}
asmetrics.RegisterSandboxCollector(ctx, mgr.GetClient(), mgr.GetLogger().WithName("sandbox-collector"))

if err := o.setupControllers(mgr, instrumenter, podMutator); err != nil {
Expand Down
94 changes: 39 additions & 55 deletions examples/lifecycle/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import (

sandboxv1beta1 "github.com/cocoonstack/sandbox-operator/api/v1beta1"
extv1beta1 "github.com/cocoonstack/sandbox-operator/extensions/api/v1beta1"
"github.com/cocoonstack/sandbox-operator/pkg/scale"
)

const (
Expand All @@ -58,8 +59,7 @@ const (
// publish does not fail the walk-through.
visibilityTimeout = 90 * time.Second

// claimIDAnnotation carries the node-local claim id of a delivered sandbox.
claimIDAnnotation = "sandbox.cocoonstack.io/claim-id"
claimIDAnnotation = scale.ClaimIDAnnotation
)

type options struct {
Expand Down Expand Up @@ -176,13 +176,11 @@ func runKubernetes(ctx context.Context, c client.Client, rc rest.Interface, o op
}
sb.Spec.PodTemplate.Spec.Containers = []corev1.Container{{Name: "agent", Image: o.template}}

// 1. Create — a claim against a warm pool, not a scheduling decision.
if err := c.Create(ctx, sb); err != nil {
return fmt.Errorf("create Sandbox: %w", err)
}
stepf("create", "Sandbox %s/%s", o.namespace, name)

// 2. Wait until the read view publishes it, then Get and List.
live, err := waitVisible(ctx, c, o.namespace, name)
if err != nil {
return err
Expand All @@ -195,16 +193,13 @@ func runKubernetes(ctx context.Context, c client.Client, rc rest.Interface, o op
}
stepf("list", "%d sandbox(es) in %s", len(list.Items), o.namespace)

// 3. snapshot — an immutable checkpoint; the source keeps running.
snap := &sandboxv1beta1.SandboxSnapshotResult{}
if err := post(ctx, rc, o.namespace, name, "snapshot",
&sandboxv1beta1.SandboxSnapshotOptions{Name: "example-checkpoint"}, snap); err != nil {
return fmt.Errorf("snapshot: %w", err)
}
stepf("snapshot", "snapshotID=%s on node=%s", snap.SnapshotID, snap.NodeName)

// 4. fork — the source is checkpointed in place and keeps running; each
// child is a brand-new sandbox with its own id and lease.
forked := &sandboxv1beta1.SandboxForkResult{}
if err := post(ctx, rc, o.namespace, name, "fork",
&sandboxv1beta1.SandboxForkOptions{Count: 2, TTLSeconds: 600}, forked); err != nil {
Expand All @@ -214,23 +209,18 @@ func runKubernetes(ctx context.Context, c client.Client, rc rest.Interface, o op
stepf("fork", "child[%d] sandboxID=%s node=%s", i, child.SandboxID, child.NodeName)
}

// 5. pause — writes the guest's memory out and stops the VM, so this is
// the slow verb: its cost is proportional to guest RAM.
start := time.Now()
if err := post(ctx, rc, o.namespace, name, "pause", &sandboxv1beta1.SandboxPauseOptions{}, nil); err != nil {
return fmt.Errorf("pause: %w", err)
}
stepf("pause", "took %s (proportional to guest memory)", time.Since(start).Round(time.Millisecond))

// 6. resume — cocoon's mmap restore fast path, and idempotent on a
// sandbox that is already running.
start = time.Now()
if err := post(ctx, rc, o.namespace, name, "resume", &sandboxv1beta1.SandboxResumeOptions{}, nil); err != nil {
return fmt.Errorf("resume: %w", err)
}
stepf("resume", "took %s (mmap restore fast path)", time.Since(start).Round(time.Millisecond))

// 7. Delete — releases the claim back to the node's pool.
if o.keep {
stepf("delete", "skipped (-keep)")
return nil
Expand All @@ -247,25 +237,21 @@ func runKubernetes(ctx context.Context, c client.Client, rc rest.Interface, o op
// served from NodeInventory, which is republished on a ~30s cadence — so a
// caller that reads immediately after creating must expect a NotFound.
func waitVisible(ctx context.Context, c client.Client, ns, name string) (*sandboxv1beta1.Sandbox, error) {
deadline := time.Now().Add(visibilityTimeout)
for {
var sb sandboxv1beta1.Sandbox
var sb sandboxv1beta1.Sandbox
err := pollVisible(ctx, fmt.Sprintf("sandbox %s/%s", ns, name), func() (bool, error) {
err := c.Get(ctx, client.ObjectKey{Namespace: ns, Name: name}, &sb)
if err == nil {
return &sb, nil
}
if !apierrors.IsNotFound(err) {
return nil, fmt.Errorf("get Sandbox: %w", err)
if apierrors.IsNotFound(err) {
return false, nil
}
if time.Now().After(deadline) {
return nil, fmt.Errorf("sandbox %s/%s not visible within %s", ns, name, visibilityTimeout)
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(3 * time.Second):
if err != nil {
return false, fmt.Errorf("get Sandbox: %w", err)
}
return true, nil
})
if err != nil {
return nil, err
}
return &sb, nil
}

// post invokes an action subresource. These are POST-only verbs (the
Expand Down Expand Up @@ -313,15 +299,12 @@ func runE2B(ctx context.Context, o options) error {
}
stepf("health", "reachable")

// 1. Templates — the pools this fleet can serve claims from; a templateID
// from here is what create accepts.
var templates []map[string]any
if err := e.do(ctx, http.MethodGet, "/templates", nil, &templates); err != nil {
return fmt.Errorf("list templates: %w", err)
}
stepf("templates", "%d available", len(templates))

// 2. Create.
var created map[string]any
if err := e.do(ctx, http.MethodPost, "/sandboxes",
map[string]any{"templateID": o.template, "timeout": 600}, &created); err != nil {
Expand All @@ -337,7 +320,6 @@ func runE2B(ctx context.Context, o options) error {
return fmt.Errorf("sandboxID %q is not DNS-label safe", id)
}

// 3. List and Get.
var listed []map[string]any
if err := e.do(ctx, http.MethodGet, "/sandboxes", nil, &listed); err != nil {
return fmt.Errorf("list: %w", err)
Expand All @@ -349,7 +331,6 @@ func runE2B(ctx context.Context, o options) error {
}
stepf("get", "%s is in the read view", id)

// 4. Metrics.
var metrics []map[string]any
if err := e.do(ctx, http.MethodGet, "/sandboxes/"+id+"/metrics", nil, &metrics); err != nil {
return fmt.Errorf("metrics: %w", err)
Expand All @@ -358,7 +339,6 @@ func runE2B(ctx context.Context, o options) error {
stepf("metrics", "cpuCount=%v memTotal=%v", metrics[0]["cpuCount"], metrics[0]["memTotal"])
}

// 5. Snapshot, then list snapshots.
var snap map[string]any
if err := e.do(ctx, http.MethodPost, "/sandboxes/"+id+"/snapshots",
map[string]any{"name": "example-e2b-snap"}, &snap); err != nil {
Expand All @@ -372,16 +352,13 @@ func runE2B(ctx context.Context, o options) error {
}
stepf("snapshots", "%d checkpoint(s) fleet-wide", len(snaps))

// 6. Fork — one result per child, each a new sandbox.
var forks []map[string]any
if err := e.do(ctx, http.MethodPost, "/sandboxes/"+id+"/fork",
map[string]any{"count": 2, "timeout": 600}, &forks); err != nil {
return fmt.Errorf("fork: %w", err)
}
stepf("fork", "%d child sandbox(es)", len(forks))

// 7. Pause, and prove the already-paused contract: the SDK reads 409 as
// "was already paused" and returns false rather than raising.
if code, err := e.status(ctx, http.MethodPost, "/sandboxes/"+id+"/pause", nil); err != nil {
return err
} else if code != http.StatusNoContent {
Expand All @@ -396,8 +373,6 @@ func runE2B(ctx context.Context, o options) error {
}
stepf("pause", "409 on repeat — the already-paused contract holds")

// 8. Connect is the SDK's resume: 201 when it actually restored a paused
// sandbox, 200 when it was already running.
if code, err := e.status(ctx, http.MethodPost, "/sandboxes/"+id+"/connect",
map[string]any{"timeout": 600}); err != nil {
return err
Expand All @@ -414,7 +389,6 @@ func runE2B(ctx context.Context, o options) error {
}
stepf("connect", "200 — already running, no restore")

// 9. setTimeout and the keepalive.
if code, err := e.status(ctx, http.MethodPost, "/sandboxes/"+id+"/timeout",
map[string]any{"timeout": 900}); err != nil {
return err
Expand All @@ -431,7 +405,6 @@ func runE2B(ctx context.Context, o options) error {
}
stepf("refreshes", "keepalive accepted")

// 10. Delete.
if o.keep {
stepf("delete", "skipped (-keep)")
return nil
Expand Down Expand Up @@ -468,24 +441,13 @@ func (e *e2bClient) health(ctx context.Context) error {
// waitVisible polls until the read view publishes the sandbox — the same
// eventual consistency the Kubernetes surface has, for the same reason.
func (e *e2bClient) waitVisible(ctx context.Context, id string) error {
deadline := time.Now().Add(visibilityTimeout)
for {
return pollVisible(ctx, "sandbox "+id, func() (bool, error) {
code, err := e.status(ctx, http.MethodGet, "/sandboxes/"+id, nil)
if err != nil {
return err
}
if code == http.StatusOK {
return nil
return false, err
}
if time.Now().After(deadline) {
return fmt.Errorf("sandbox %s not visible within %s", id, visibilityTimeout)
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(3 * time.Second):
}
}
return code == http.StatusOK, nil
})
}

func (e *e2bClient) request(ctx context.Context, method, path string, body any) (*http.Request, error) {
Expand Down Expand Up @@ -551,6 +513,28 @@ func (e *e2bClient) status(ctx context.Context, method, path string, body any) (
return resp.StatusCode, nil
}

// pollVisible retries probe on a 3s tick until the subject is visible or visibilityTimeout elapses.
func pollVisible(ctx context.Context, subject string, probe func() (bool, error)) error {
deadline := time.Now().Add(visibilityTimeout)
for {
visible, err := probe()
if err != nil {
return err
}
if visible {
return nil
}
if time.Now().After(deadline) {
return fmt.Errorf("%s not visible within %s", subject, visibilityTimeout)
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(3 * time.Second):
}
}
}

func section(name string) { fmt.Printf("\n=== %s ===\n", name) }

func stepf(verb, format string, args ...any) {
Expand Down
Loading