runtime

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

Documentation

Overview

Package runtime manages challenge workload containers behind the ChallengeRuntime interface. v0.1 implementation: DockerRuntime. The Manager layers DB-backed instance lifecycle (port allocation, rows, connection info) over the interface. Scope: per-event shared instances (one container per container-kind challenge). See docs/v0.1/08-challenge-runtime.md.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrImagePull     = Unavailable(fmt.Errorf("fake: image pull failed"))
	ErrPortBound     = fmt.Errorf("fake: host port already bound")
	ErrNetworkCreate = Unavailable(fmt.Errorf("fake: network create failed"))
	ErrDaemonTimeout = Unavailable(fmt.Errorf("fake: daemon timeout"))
)

Injectable deploy-time faults (set FakeRuntime.DeployFault to one).

Functions

func IsUnavailable

func IsUnavailable(err error) bool

IsUnavailable reports whether err is a runtime-unavailable condition.

func Reconcile

func Reconcile(observed []ContainerView, rows []InstanceRow, networks []NetworkView, now time.Time) ([]Action, ReconcileStats)

Reconcile decides how to align the daemon's observed state with the DB rows.

Division of labour with the reaper (ReapStaleOnce, which deletes pending/error rows past ReapAfter): reconcile NEVER touches a pending row and never deletes a row — the two operate on disjoint states, so a slow deploy's pending row is only ever reaped (age-gated, ReapAfter ≫ deploy timeout), never marked lost mid-deploy. Two guards keep a still-recording deploy safe: a row updated within reconcileGrace is skipped entirely, and a row with no committed ContainerID never has its container removed regardless of age. Team-network GC is likewise held off while that team has a pending or fresh row.

func Unavailable

func Unavailable(cause error) error

Unavailable wraps an error as a runtime-unavailable condition.

Types

type Action

type Action struct {
	Kind        ActionKind
	InstanceID  string
	ContainerID string
	NetworkID   string
	Reason      string // for logging/audit
}

Action is one reconcile decision. Which id field is set depends on Kind: MarkLost→InstanceID, RemoveContainer/FlagUnadopted→ContainerID, RemoveNetwork→NetworkID.

type ActionKind

type ActionKind int

ActionKind enumerates the reconcile decisions.

const (
	// ActionMarkLost sets a row to 'lost' because its container has vanished.
	ActionMarkLost ActionKind = iota
	// ActionRemoveContainer removes a managed container that no live row owns (an
	// orphan) or that a row has superseded (a stale container from an older spec).
	ActionRemoveContainer
	// ActionRemoveNetwork removes an empty per-team bridge.
	ActionRemoveNetwork
	// ActionFlagUnadopted reports a managed container whose instance_id label is
	// missing/unparseable — never removed, but surfaced (metric, log, fleet view) so
	// an organizer sees a container silently holding a port.
	ActionFlagUnadopted
	// ActionFlagUnadoptedNetwork reports a team bridge with no resolvable team_id
	// (e.g. created by a pre-team_id-label release). Never GC'd — without the team we
	// cannot tell an in-flight deploy from an abandoned bridge — but surfaced like an
	// unadopted container so an organizer can clean it up.
	ActionFlagUnadoptedNetwork
)

type ChallengeRuntime

type ChallengeRuntime interface {
	// Name returns a stable identifier, e.g. "docker".
	Name() string
	// Deploy creates and starts the instance for a challenge. Idempotent:
	// deploying an already-running instance returns it unchanged.
	Deploy(ctx context.Context, spec InstanceSpec) (Instance, error)
	// Stop halts the container but keeps it for restart/log inspection.
	Stop(ctx context.Context, instanceID uuid.UUID) error
	// Destroy stops and removes the container and frees its port.
	Destroy(ctx context.Context, instanceID uuid.UUID) error
	// Status re-inspects the live container and returns current state.
	Status(ctx context.Context, instanceID uuid.UUID) (Instance, error)
	// Logs returns up to tailLines of recent container output.
	Logs(ctx context.Context, instanceID uuid.UUID, tailLines int) (string, error)
	// Reconcile aligns tracked instances with actual runtime state.
	Reconcile(ctx context.Context) error
}

ChallengeRuntime manages challenge workload containers. This is the future plugin surface — kept deliberately minimal.

type ContainerView

type ContainerView struct {
	ContainerID string
	// InstanceID is the osctf.instance_id label. Empty when the label is missing or
	// unparseable — such a container is never removed (we cannot identify it) but is
	// flagged unadopted so it does not hold its port invisibly.
	InstanceID string
	Running    bool
}

ContainerView is the reconcile-relevant projection of one osctf.managed container.

type DeployReq

type DeployReq struct {
	ChallengeID uuid.UUID
	TeamID      uuid.UUID
	Flag        string     // per-instance flag (per_instance) or the challenge flag (static)
	ExpiresAt   *time.Time // nil = no TTL
}

DeployReq is a per-team deploy request from the scheduler.

type DockerRuntime

type DockerRuntime struct {
	// contains filtered or unexported fields
}

DockerRuntime implements ChallengeRuntime against the local Docker daemon. It holds the instances store so it can update rows as containers change state.

func NewDockerRuntime

func NewDockerRuntime(q *gen.Queries, log *slog.Logger, dockerHost string) (*DockerRuntime, error)

NewDockerRuntime connects to Docker (honoring DOCKER_HOST) and negotiates the API version. It does not fail the platform if Docker is down — callers get a runtime-unavailable error at operation time.

func (*DockerRuntime) Deploy

func (d *DockerRuntime) Deploy(ctx context.Context, spec InstanceSpec) (inst Instance, err error)

Deploy implements ChallengeRuntime: pull-if-missing, create, start, health-check.

func (*DockerRuntime) Destroy

func (d *DockerRuntime) Destroy(ctx context.Context, instanceID uuid.UUID) error

Destroy implements ChallengeRuntime: remove the container (row deleted by Manager).

func (*DockerRuntime) ListUnadopted

func (d *DockerRuntime) ListUnadopted(ctx context.Context) ([]UnadoptedContainer, error)

ListUnadopted returns managed containers whose osctf.instance_id label is missing or unparseable — the ones reconcile flags but never removes. Surfaced in the admin fleet view. A daemon-down error is swallowed (empty list, not an error).

func (*DockerRuntime) ListUnadoptedNetworks

func (d *DockerRuntime) ListUnadoptedNetworks(ctx context.Context) ([]UnadoptedNetwork, error)

ListUnadoptedNetworks returns managed per-team bridges with no resolvable team_id label — the ones reconcile flags but never GC's. Surfaced in the admin fleet view.

func (*DockerRuntime) Logs

func (d *DockerRuntime) Logs(ctx context.Context, instanceID uuid.UUID, tail int) (string, error)

Logs implements ChallengeRuntime.

func (*DockerRuntime) Name

func (d *DockerRuntime) Name() string

Name implements ChallengeRuntime.

func (*DockerRuntime) Reconcile

func (d *DockerRuntime) Reconcile(ctx context.Context) error

Reconcile implements ChallengeRuntime: align DB rows with live containers. Reconcile aligns the daemon with the DB in three steps: gather what the daemon and DB show, call the pure Reconcile decision, execute the returned Actions. Health grading of still-present instances is a separate live-inspect side effect (refreshHealth), kept out of the pure decision.

func (*DockerRuntime) Status

func (d *DockerRuntime) Status(ctx context.Context, instanceID uuid.UUID) (Instance, error)

Status implements ChallengeRuntime: inspect the live container and update the row.

func (*DockerRuntime) Stop

func (d *DockerRuntime) Stop(ctx context.Context, instanceID uuid.UUID) error

Stop implements ChallengeRuntime.

func (*DockerRuntime) VerifyIsolation

func (d *DockerRuntime) VerifyIsolation(ctx context.Context) (bool, error)

VerifyIsolation runs a one-shot self-check of cross-network isolation: it stands up two throwaway per-team-style bridges (masquerade off), a listener that PUBLISHES a host port on one, and probes it from the other. Publishing a port is what defeats cross-network isolation on Docker Desktop's VM (verified: native Linux blocks the cross-bridge probe via DOCKER-ISOLATION, Docker Desktop does not), so the check mirrors a real instance by publishing.

Returns (isolated, nil) on a conclusive result. A non-nil error means the check could not run (image/daemon/timeout) — the caller must treat isolation as unknown, not as a breach. The throwaway resources carry no osctf.managed label so Reconcile/gcTeamNetworks leave them alone; they are cleaned up here.

type FakeRuntime

type FakeRuntime struct {

	// FailDeploy, when set, makes Deploy mark the row errored (tests error paths).
	FailDeploy bool
	// Unavailable, when set, makes every op return a runtime-unavailable error.
	Unavailable bool
	// DeployFault, when set, makes Deploy return it before touching any state — the
	// deploy-time faults: image-pull failure, host-port-already-bound, network-create
	// failure, daemon timeout (use the Err* sentinels below).
	DeployFault error
	// BeforeDeploy, when set, is invoked at the start of Deploy (before any state
	// change). It is the injectable per-operation delay/latency mechanism (reused from
	// 1a — a test blocks or sleeps in it); it also lets a test block a specific deploy.
	BeforeDeploy func(ctx context.Context, spec InstanceSpec)
	// FailReconcileActionFor, when set and true for an action, makes Reconcile's
	// executor skip that action (simulating a mid-sequence failure) and CONTINUE with
	// the rest — the chosen partial-failure policy (best-effort, converge next pass).
	FailReconcileActionFor func(a Action) bool
	// contains filtered or unexported fields
}

FakeRuntime is an in-DB simulation used by handler/service tests that must not touch Docker. It moves instance rows through their states without containers.

func NewFakeRuntime

func NewFakeRuntime(q *gen.Queries) *FakeRuntime

NewFakeRuntime builds a fake over the instances store.

func NewFakeRuntimeWithClock

func NewFakeRuntimeWithClock(q *gen.Queries, now func() time.Time) *FakeRuntime

NewFakeRuntimeWithClock builds a fake whose started_at/health timestamps come from the given clock, so tests injecting a clock into the scheduler see a consistent timeline (started_at aligns with the scheduler's now).

func (*FakeRuntime) ContainerIDs

func (f *FakeRuntime) ContainerIDs() []uuid.UUID

ContainerIDs returns the instance ids the fake currently has a container for (test visibility into the simulated daemon state).

func (*FakeRuntime) Deploy

func (f *FakeRuntime) Deploy(ctx context.Context, spec InstanceSpec) (Instance, error)

Deploy implements ChallengeRuntime.

func (*FakeRuntime) DeployedSpecs

func (f *FakeRuntime) DeployedSpecs() []InstanceSpec

DeployedSpecs returns a copy of every spec passed to Deploy (concurrency-safe).

func (*FakeRuntime) Destroy

func (f *FakeRuntime) Destroy(_ context.Context, instanceID uuid.UUID) error

Destroy implements ChallengeRuntime (row deletion is the Manager's job).

func (*FakeRuntime) DestroyedIDs

func (f *FakeRuntime) DestroyedIDs() []uuid.UUID

DestroyedIDs returns a copy of every instance id passed to Destroy.

func (*FakeRuntime) ExitContainer

func (f *FakeRuntime) ExitContainer(instanceID uuid.UUID)

ExitContainer simulates a container that exited immediately after start (still present, no longer running) — health grading, not reconcile, acts on it.

func (*FakeRuntime) InjectContainer

func (f *FakeRuntime) InjectContainer(instanceID uuid.UUID, containerID string)

InjectContainer adds a simulated container with no backing row — an orphan the next Reconcile should remove.

func (*FakeRuntime) Logs

func (f *FakeRuntime) Logs(_ context.Context, instanceID uuid.UUID, _ int) (string, error)

Logs implements ChallengeRuntime.

func (*FakeRuntime) Name

func (f *FakeRuntime) Name() string

Name implements ChallengeRuntime.

func (*FakeRuntime) Reconcile

func (f *FakeRuntime) Reconcile(ctx context.Context) error

Reconcile implements ChallengeRuntime by running the SAME pure Reconcile decision as DockerRuntime, gathered from the fake's simulated container set and the DB rows, then executing the actions (mark lost, drop a stale/orphan container from the set). Networks and unadopted-flagging are Docker-only, so they are no-ops here.

func (*FakeRuntime) ReconcileActionCount

func (f *FakeRuntime) ReconcileActionCount() int

ReconcileActionCount returns how many actions the most recent Reconcile applied.

func (*FakeRuntime) Status

func (f *FakeRuntime) Status(ctx context.Context, instanceID uuid.UUID) (Instance, error)

Status implements ChallengeRuntime.

func (*FakeRuntime) Stop

func (f *FakeRuntime) Stop(ctx context.Context, instanceID uuid.UUID) error

Stop implements ChallengeRuntime.

func (*FakeRuntime) VanishContainer

func (f *FakeRuntime) VanishContainer(instanceID uuid.UUID)

VanishContainer simulates a container disappearing from the daemon WITHOUT its row changing (crash, manual docker rm) — the next Reconcile should mark the row lost.

type Instance

type Instance struct {
	ID           uuid.UUID
	ChallengeID  uuid.UUID
	TeamID       *uuid.UUID
	State        State
	ContainerID  string
	HostPort     int
	Network      string
	StartedAt    *time.Time
	LastHealthAt *time.Time
	ExpiresAt    *time.Time
	Err          string
}

Instance is the observed state of a challenge instance.

type InstanceRow

type InstanceRow struct {
	InstanceID string
	TeamID     string // "" for shared instances
	State      string
	// ContainerID is the container the row believes it owns; empty when the deploy
	// has not written it yet (still creating the container).
	ContainerID string
	UpdatedAt   time.Time
}

InstanceRow is the reconcile-relevant projection of one DB instance row.

type InstanceSpec

type InstanceSpec struct {
	InstanceID   uuid.UUID
	ChallengeID  uuid.UUID
	Slug         string
	Image        string
	InternalPort int
	HostPort     int
	MemLimitMB   int
	CPUMillis    int
	Env          map[string]string // already carries FLAG (per-instance value when set)

	TeamID         *uuid.UUID // nil = shared instance (v0.1); set = per-team
	NetworkName    string     // docker network to attach; "" = shared 'osctf-challenges'
	NoEgress       bool       // true -> network created without outbound NAT (egress off)
	ReadonlyRootfs bool       // true -> read-only container rootfs
	Tmpfs          []string   // writable tmpfs mount targets, e.g. ["/tmp"]
}

InstanceSpec is the desired shape of a challenge instance. The v0.2 fields (owner, network, hardening) are additive; the Manager fills them from the challenge row so the runtime never has to know about instancing or flag mode.

type Manager

type Manager struct {
	// contains filtered or unexported fields
}

Manager layers DB-backed instance lifecycle over a ChallengeRuntime. It owns port allocation, instance rows, spec construction (including v0.2 hardening), and connection-info rendering. The shared path (team_id NULL) is v0.1 behaviour; the per-team path is driven by the scheduler.

func NewManager

func NewManager(rt ChallengeRuntime, q *gen.Queries, publicHost string, portStart, portEnd int) *Manager

NewManager builds the manager.

func (*Manager) ConfigureIsolationGate

func (m *Manager) ConfigureIsolationGate(allowUnisolated bool, log *slog.Logger)

ConfigureIsolationGate wires the override + a logger for the isolation gate, and logs loudly at boot when the override is set — so "we ran unisolated" is on the record from the first line. With no override (the default) container deploys fail closed until the boot self-check reports isolation enforced (see RecordIsolationVerdict). Call once at startup.

func (*Manager) ConnectionInfo

func (m *Manager) ConnectionInfo(ch gen.Challenge, inst Instance) string

ConnectionInfo renders the participant connection string, or "" unless running.

func (*Manager) CountTeamRunning

func (m *Manager) CountTeamRunning(ctx context.Context, teamID uuid.UUID) (int, error)

CountTeamRunning returns the number of running instances a team holds.

func (*Manager) Deploy

func (m *Manager) Deploy(ctx context.Context, challengeID uuid.UUID) (Instance, error)

Deploy provisions (or returns the running) shared instance for a container challenge.

func (*Manager) DeployForTeam

func (m *Manager) DeployForTeam(ctx context.Context, req DeployReq) (Instance, error)

DeployForTeam provisions (or returns the running) instance for a (challenge, team). The scheduler owns quota, flag generation, and TTL; the Manager owns rows, ports, networks, and hardening.

func (*Manager) Destroy

func (m *Manager) Destroy(ctx context.Context, challengeID uuid.UUID) error

Destroy removes the shared container and deletes the row (freeing its port).

func (*Manager) DestroyForChallenge

func (m *Manager) DestroyForChallenge(ctx context.Context, challengeID uuid.UUID) error

DestroyForChallenge is Destroy but a no-op when no shared instance exists.

func (*Manager) DestroyInstance

func (m *Manager) DestroyInstance(ctx context.Context, instanceID uuid.UUID) error

DestroyInstance removes a container by instance id and deletes its row.

func (*Manager) Get

func (m *Manager) Get(ctx context.Context, challengeID uuid.UUID) (Instance, bool, error)

Get returns the shared instance for a challenge, refreshing its live status.

func (*Manager) GetTeamInstance

func (m *Manager) GetTeamInstance(ctx context.Context, challengeID, teamID uuid.UUID) (Instance, bool, error)

GetTeamInstance returns a team's instance for a challenge (from the row only).

func (*Manager) InstanceForChallenge

func (m *Manager) InstanceForChallenge(ctx context.Context, challengeID uuid.UUID) (Instance, bool)

InstanceForChallenge returns the shared instance (from the row only) for participant payloads; ok=false when there is none.

func (*Manager) ListAll

func (m *Manager) ListAll(ctx context.Context) ([]Instance, error)

ListAll returns every instance row (shared + per-team) for the admin fleet view.

func (*Manager) ListStale

func (m *Manager) ListStale(ctx context.Context, olderThan time.Duration) ([]Instance, error)

ListStale returns instances stuck in pending/error for longer than olderThan. allocateRow reserves a host_port before Deploy; a failed or interrupted Deploy leaves that row behind, and ListUsedPorts counts its port whatever the state, so a stuck row leaks a port until it is destroyed. The caller destroys them (freeing the port). olderThan must comfortably exceed the Deploy timeout so a row that is legitimately mid-deploy is never returned. Timestamps are DB wall-clock, so the cutoff is computed from time.Now, not any injected scheduler clock.

func (*Manager) ListTeamInstances

func (m *Manager) ListTeamInstances(ctx context.Context, teamID uuid.UUID) ([]Instance, error)

ListTeamInstances returns all instances a team owns.

func (*Manager) ListUnadopted

func (m *Manager) ListUnadopted(ctx context.Context) ([]UnadoptedContainer, error)

ListUnadopted returns managed containers reconcile could not resolve to a row. Runtimes without containers (the fake) report none.

func (*Manager) ListUnadoptedNetworks

func (m *Manager) ListUnadoptedNetworks(ctx context.Context) ([]UnadoptedNetwork, error)

ListUnadoptedNetworks returns per-team bridges with no resolvable team_id.

func (*Manager) Logs

func (m *Manager) Logs(ctx context.Context, challengeID uuid.UUID, tail int) (string, error)

Logs returns recent output from the shared container.

func (*Manager) Reconcile

func (m *Manager) Reconcile(ctx context.Context) error

Reconcile runs one reconciliation pass.

func (*Manager) RecordIsolationVerdict

func (m *Manager) RecordIsolationVerdict(isolated bool)

RecordIsolationVerdict stores the boot self-check result. Until it is called the verdict is UNKNOWN and container deploys fail closed; a verification that errors should leave it UNKNOWN.

func (*Manager) Restart

func (m *Manager) Restart(ctx context.Context, challengeID uuid.UUID) (Instance, error)

Restart stops then re-deploys the challenge's shared instance.

func (*Manager) Runtime

func (m *Manager) Runtime() ChallengeRuntime

Runtime exposes the underlying runtime (for the reconcile ticker).

func (*Manager) VerifyIsolation

func (m *Manager) VerifyIsolation(ctx context.Context) (bool, error)

VerifyIsolation runs the runtime's cross-network isolation self-check. Runtimes that cannot check (e.g. the fake) report isolated=true — the warning is only meaningful against a real container network.

type NetworkView

type NetworkView struct {
	NetworkID   string
	Name        string
	TeamID      string // owning team (osctf.team_id label); "" if unlabeled/legacy
	TeamNetwork bool   // per-team bridge (osctf.team_network label / osctf-team- prefix)
	Attached    int    // number of attached containers
}

NetworkView is the reconcile-relevant projection of one osctf.managed network.

type ReconcileStats

type ReconcileStats struct {
	SkippedGrace int // rows left alone because within grace (includes FutureRows)
	FutureRows   int // rows whose updated_at is AHEAD of now — a clock-skew anomaly
}

ReconcileStats reports what a pass saw, for observability: it must be visible when a pass skips everything (e.g. clock skew) or when it silently does nothing.

type State

type State string

State is a challenge instance's lifecycle state.

const (
	StatePending   State = "pending"
	StateStarting  State = "starting"
	StateRunning   State = "running"
	StateUnhealthy State = "unhealthy"
	StateStopped   State = "stopped"
	StateError     State = "error"
	StateLost      State = "lost"
)

type UnadoptedContainer

type UnadoptedContainer struct {
	ContainerID string
	Image       string
	CreatedAt   time.Time
}

UnadoptedContainer is a managed container reconcile could not resolve to an instance (missing/unparseable osctf.instance_id) — surfaced in the admin fleet view so an organizer can see a container silently holding a host port.

type UnadoptedNetwork

type UnadoptedNetwork struct {
	NetworkID string
	Name      string
}

UnadoptedNetwork is a per-team bridge with no resolvable team_id (e.g. from a pre-team_id-label release). Never GC'd; surfaced for manual cleanup.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL