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 ¶
- Variables
- func IsUnavailable(err error) bool
- func Reconcile(observed []ContainerView, rows []InstanceRow, networks []NetworkView, ...) ([]Action, ReconcileStats)
- func Unavailable(cause error) error
- type Action
- type ActionKind
- type ChallengeRuntime
- type ContainerView
- type DeployReq
- type DockerRuntime
- func (d *DockerRuntime) Deploy(ctx context.Context, spec InstanceSpec) (inst Instance, err error)
- func (d *DockerRuntime) Destroy(ctx context.Context, instanceID uuid.UUID) error
- func (d *DockerRuntime) ListUnadopted(ctx context.Context) ([]UnadoptedContainer, error)
- func (d *DockerRuntime) ListUnadoptedNetworks(ctx context.Context) ([]UnadoptedNetwork, error)
- func (d *DockerRuntime) Logs(ctx context.Context, instanceID uuid.UUID, tail int) (string, error)
- func (d *DockerRuntime) Name() string
- func (d *DockerRuntime) Reconcile(ctx context.Context) error
- func (d *DockerRuntime) Status(ctx context.Context, instanceID uuid.UUID) (Instance, error)
- func (d *DockerRuntime) Stop(ctx context.Context, instanceID uuid.UUID) error
- func (d *DockerRuntime) VerifyIsolation(ctx context.Context) (bool, error)
- type FakeRuntime
- func (f *FakeRuntime) ContainerIDs() []uuid.UUID
- func (f *FakeRuntime) Deploy(ctx context.Context, spec InstanceSpec) (Instance, error)
- func (f *FakeRuntime) DeployedSpecs() []InstanceSpec
- func (f *FakeRuntime) Destroy(_ context.Context, instanceID uuid.UUID) error
- func (f *FakeRuntime) DestroyedIDs() []uuid.UUID
- func (f *FakeRuntime) ExitContainer(instanceID uuid.UUID)
- func (f *FakeRuntime) InjectContainer(instanceID uuid.UUID, containerID string)
- func (f *FakeRuntime) Logs(_ context.Context, instanceID uuid.UUID, _ int) (string, error)
- func (f *FakeRuntime) Name() string
- func (f *FakeRuntime) Reconcile(ctx context.Context) error
- func (f *FakeRuntime) ReconcileActionCount() int
- func (f *FakeRuntime) Status(ctx context.Context, instanceID uuid.UUID) (Instance, error)
- func (f *FakeRuntime) Stop(ctx context.Context, instanceID uuid.UUID) error
- func (f *FakeRuntime) VanishContainer(instanceID uuid.UUID)
- type Instance
- type InstanceRow
- type InstanceSpec
- type Manager
- func (m *Manager) ConfigureIsolationGate(allowUnisolated bool, log *slog.Logger)
- func (m *Manager) ConnectionInfo(ch gen.Challenge, inst Instance) string
- func (m *Manager) CountTeamRunning(ctx context.Context, teamID uuid.UUID) (int, error)
- func (m *Manager) Deploy(ctx context.Context, challengeID uuid.UUID) (Instance, error)
- func (m *Manager) DeployForTeam(ctx context.Context, req DeployReq) (Instance, error)
- func (m *Manager) Destroy(ctx context.Context, challengeID uuid.UUID) error
- func (m *Manager) DestroyForChallenge(ctx context.Context, challengeID uuid.UUID) error
- func (m *Manager) DestroyInstance(ctx context.Context, instanceID uuid.UUID) error
- func (m *Manager) Get(ctx context.Context, challengeID uuid.UUID) (Instance, bool, error)
- func (m *Manager) GetTeamInstance(ctx context.Context, challengeID, teamID uuid.UUID) (Instance, bool, error)
- func (m *Manager) InstanceForChallenge(ctx context.Context, challengeID uuid.UUID) (Instance, bool)
- func (m *Manager) ListAll(ctx context.Context) ([]Instance, error)
- func (m *Manager) ListStale(ctx context.Context, olderThan time.Duration) ([]Instance, error)
- func (m *Manager) ListTeamInstances(ctx context.Context, teamID uuid.UUID) ([]Instance, error)
- func (m *Manager) ListUnadopted(ctx context.Context) ([]UnadoptedContainer, error)
- func (m *Manager) ListUnadoptedNetworks(ctx context.Context) ([]UnadoptedNetwork, error)
- func (m *Manager) Logs(ctx context.Context, challengeID uuid.UUID, tail int) (string, error)
- func (m *Manager) Reconcile(ctx context.Context) error
- func (m *Manager) RecordIsolationVerdict(isolated bool)
- func (m *Manager) Restart(ctx context.Context, challengeID uuid.UUID) (Instance, error)
- func (m *Manager) Runtime() ChallengeRuntime
- func (m *Manager) VerifyIsolation(ctx context.Context) (bool, error)
- type NetworkView
- type ReconcileStats
- type State
- type UnadoptedContainer
- type UnadoptedNetwork
Constants ¶
This section is empty.
Variables ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) 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 ¶
Status implements ChallengeRuntime: inspect the live container and update the row.
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 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 ¶
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) 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) 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 ¶
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 ¶
ConnectionInfo renders the participant connection string, or "" unless running.
func (*Manager) CountTeamRunning ¶
CountTeamRunning returns the number of running instances a team holds.
func (*Manager) Deploy ¶
Deploy provisions (or returns the running) shared instance for a container challenge.
func (*Manager) DeployForTeam ¶
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 ¶
Destroy removes the shared container and deletes the row (freeing its port).
func (*Manager) DestroyForChallenge ¶
DestroyForChallenge is Destroy but a no-op when no shared instance exists.
func (*Manager) DestroyInstance ¶
DestroyInstance removes a container by instance id and deletes its row.
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 ¶
InstanceForChallenge returns the shared instance (from the row only) for participant payloads; ok=false when there is none.
func (*Manager) ListAll ¶
ListAll returns every instance row (shared + per-team) for the admin fleet view.
func (*Manager) ListStale ¶
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 ¶
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) RecordIsolationVerdict ¶
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) Runtime ¶
func (m *Manager) Runtime() ChallengeRuntime
Runtime exposes the underlying runtime (for the reconcile ticker).
func (*Manager) VerifyIsolation ¶
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 UnadoptedContainer ¶
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 ¶
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.