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/adr/0011-v2-k3d-user-namespaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ v2 runs sandboxes as pods in a local k3d cluster. k3d runs its node containers p

## Decision

- Every sandbox pod runs with `hostUsers: false`: pod root maps to an unprivileged host UID range (verified on this kernel: pod root is host UID 3236954112 and up, not 0).
- Every sandbox pod runs with `hostUsers: false`: pod root maps to an unprivileged range of 65536 host UIDs, different per pod, never 0 (verified on this kernel, and checked by `scripts/k8s-prove.sh`).
- Since Phase 9 a ValidatingAdmissionPolicy denies any pod in `sandboxes` without it, for every requester (ADR 0012).
- The privileged k3d nodes are accepted as a known limit for a local, single-user cluster, with the compensations in ADR 0012: user namespaces, the admission policy, Pod Security Admission, RBAC limited to pods in `sandboxes`, default-deny NetworkPolicies, no service account token in sandboxes, and nothing published beyond the host's 127.0.0.1.
- Rootless k3d (on rootless Docker or Podman) is the future fix for node root. It needs sudo for packages and subordinate ID setup, and k3d's support for it is experimental.
Expand Down
1 change: 1 addition & 0 deletions internal/cli/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func (a *App) Serve(ctx context.Context, args []string) error {
BuildStats: a.buildStats,
Describe: func(lang string, err error) string { return Message(a.bundle, lang, err) },
Assets: web.Assets(),
CanRebuild: os.Getenv(EnvDBTMode) != "cronjob",
})
if err != nil {
return err
Expand Down
22 changes: 20 additions & 2 deletions internal/grading/grading.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ type Outcome struct {
// skipped outcome, it neither advances nor resets either streak. Fails count at
// any difficulty.
func Level(outcomes []Outcome) int {
level, _ := Progress(outcomes)
return level
}

// StreakToLevelUp is how many consecutive strong solves at the current level
// raise it (spec section 7.1).
const StreakToLevelUp = 2

// MaxLevel is the highest topic level in v1.
const MaxLevel = 3

// Progress replays outcomes like Level and also returns the current streak
// of strong solves toward the next level: 0 or 1 of StreakToLevelUp. At
// MaxLevel the streak is always 0.
func Progress(outcomes []Outcome) (level, streak int) {
level, up, down := 1, 0, 0
for _, o := range outcomes {
switch o.Kind {
Expand All @@ -80,13 +95,16 @@ func Level(outcomes []Outcome) int {
continue
}
switch {
case up >= 2 && level < 3:
case up >= StreakToLevelUp && level < MaxLevel:
level++
up, down = 0, 0
case down >= 2 && level > 1:
level--
up, down = 0, 0
}
}
return level
if level == MaxLevel {
up = 0
}
return level, up
}
26 changes: 26 additions & 0 deletions internal/grading/grading_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,29 @@ func TestLevel(t *testing.T) {
})
}
}

func TestProgress(t *testing.T) {
tests := []struct {
name string
outcomes []Outcome
level, streak int
}{
{"nothing yet", nil, 1, 0},
{"one strong at level 1", []Outcome{st(1)}, 1, 1},
{"a level up resets the streak", []Outcome{st(1), st(1)}, 2, 0},
{"one strong at level 2", []Outcome{st(1), st(1), st(2)}, 2, 1},
{"a below-level solve does not count", []Outcome{st(1), st(1), st(1)}, 2, 0},
{"a weak solve resets it", []Outcome{st(1), wk(1)}, 1, 0},
{"a fail resets it", []Outcome{st(1), fl(1)}, 1, 0},
{"no streak at the top level", []Outcome{st(1), st(1), st(2), st(2), st(3)}, 3, 0},
}
for _, tt := range tests {
level, streak := Progress(tt.outcomes)
if level != tt.level || streak != tt.streak {
t.Errorf("%s: Progress = (%d, %d), want (%d, %d)", tt.name, level, streak, tt.level, tt.streak)
}
if l := Level(tt.outcomes); l != level {
t.Errorf("%s: Level = %d, Progress level = %d", tt.name, l, level)
}
}
}
29 changes: 29 additions & 0 deletions internal/play/play.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,35 @@ func Texts(exercisesDir string, sess store.Session, lang string) (*exercise.I18n
return texts, TextVars(sess), nil
}

// Ticket is an exercise's ticket header outside a session, for the inbox and
// the catalog.
type Ticket struct {
Title string
From string
}

// CatalogTicket returns the ticket title and sender of an exercise in lang,
// before any session exists. Placeholders are filled from the first value of
// each variant, a fixed display choice that does not depend on any seed. No
// derived value exists yet, and the ticket header never uses one.
func CatalogTicket(exercisesDir, id, lang string) (Ticket, error) {
ex, err := exercise.Load(filepath.Join(exercisesDir, id))
if err != nil {
return Ticket{}, err
}
texts, err := ex.LoadI18n(lang)
if err != nil {
return Ticket{}, err
}
vars := make(map[string]string, len(ex.Variants))
for name, values := range ex.Variants {
if len(values) > 0 {
vars[name] = values[0]
}
}
return Ticket{Title: i18n.Fill(texts.Ticket.Title, vars), From: i18n.Fill(texts.Ticket.From, vars)}, nil
}

// TextVars collects variant and derived values for placeholder filling.
func TextVars(sess store.Session) map[string]string {
m := make(map[string]string)
Expand Down
21 changes: 20 additions & 1 deletion internal/runtime/scripted/scripted.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ type Runtime struct {
mu sync.Mutex
checkPasses bool
feedback string
buildHook func()
}

// SetBuildHook makes BuildImage call hook first, so a test can hold a start
// at its first step.
func (r *Runtime) SetBuildHook(hook func()) {
r.mu.Lock()
defer r.mu.Unlock()
r.buildHook = hook
}

// SetCheck makes later checks pass, or fail with the feedback key.
Expand Down Expand Up @@ -50,7 +59,17 @@ func (r *Runtime) CopyIn(context.Context, string, string, []byte) error { return
func (r *Runtime) Restart(context.Context, string) error { return nil }
func (r *Runtime) Destroy(context.Context, string) error { return nil }
func (r *Runtime) List(context.Context) ([]runtime.Sandbox, error) { return nil, nil }
func (r *Runtime) BuildImage(context.Context, string, string) error { return nil }

// BuildImage calls the build hook, if set, and succeeds.
func (r *Runtime) BuildImage(context.Context, string, string) error {
r.mu.Lock()
hook := r.buildHook
r.mu.Unlock()
if hook != nil {
hook()
}
return nil
}

// Terminal echoes stdin to stdout, like a shell printing what is typed, until
// stdin ends or ctx does.
Expand Down
Loading