package osupdates import ( "encoding/json" "strings" "testing" "time" ) var engineSet = []Package{{Name: "docker-ce", Version: "5:29.8.2-1~debian.13~trixie", Origin: "Docker"}, {Name: "containerd.io", Version: "2.3.6-1~debian.13~trixie", Origin: "Docker"}} func (f *fix) dockerNight(t *testing.T, host string, healthy bool) { t.Helper() f.dockerNightOOM(t, host, healthy, `{"result":"pass","oom_killed":true,"oom_event":true,"exit_code":137,"image":"felhom-controller","detail":""}`) } // dockerNightOOM is dockerNight with the step's memory-kill check as given ("" = an agent that reports none). func (f *fix) dockerNightOOM(t *testing.T, host string, healthy bool, oom string) { t.Helper() out := "nothing" if !healthy { out = "health_failed" } r := Report{Layer: LayerDocker, Trigger: "night", Mode: "apply", Outcome: out, Healthy: healthy, Installed: append([]Package{pk("libc6", "x")}, engineSet...)} if oom != "" { r.OOMCheck = json.RawMessage(oom) } f.ingest(t, host, r) } // The Docker set is NEVER approved automatically (`11` §5.8: the operator approves it). Red-proof: add LayerDocker to // Layers (the auto-approved list) and this fails. func TestDocker_NeverAutoApproved(t *testing.T) { f := newFix(t) for i := 0; i < 3; i++ { f.dockerNight(t, "hp", true) f.dockerNight(t, "n100", true) f.now = f.now.Add(25 * time.Hour) f.s.Evaluate() } if rel, _ := f.s.Store.LatestOSRelease(LayerDocker); rel != nil { t.Fatalf("a Docker set was auto-approved: %+v", rel) } } // The operator's button works only after every ring-0 box ran the set 2 healthy nights; an unhealthy step blocks it. // The candidate is the six engine packages only. Red-proof: compare nights against 1 instead of DockerNights and the // one-night step approves. func TestDocker_ApproveNeedsTwoHealthyNightsOnEveryRing0Box(t *testing.T) { f := newFix(t) f.dockerNight(t, "hp", true) f.dockerNight(t, "n100", true) if _, err := f.s.ApproveDocker(); err == nil || !strings.Contains(err.Error(), "1 of 2") { t.Fatalf("approved after one night: %v", err) } f.now = f.now.Add(24 * time.Hour) f.dockerNight(t, "hp", true) if _, err := f.s.ApproveDocker(); err == nil { t.Fatal("approved while n100 had only one night") } f.dockerNight(t, "n100", true) id, err := f.s.ApproveDocker() if err != nil || !strings.HasPrefix(id, "os-docker-") { t.Fatalf("%q %v", id, err) } rel, _ := f.s.Store.LatestOSRelease(LayerDocker) if rel.ApprovedBy != "operator" || !strings.Contains(rel.PackagesJSON, "docker-ce") || strings.Contains(rel.PackagesJSON, "libc6") { t.Fatalf("release %+v", rel) } if len(f.bumps) != 0 { t.Fatalf("a Docker approval must nudge no box (ring 1 takes it by a signed job): %v", f.bumps) } if b := f.s.DesiredBlock("cust1"); b.Release != nil || b.HostRelease != nil { t.Fatalf("the Docker set leaked into the desired block: %+v", b) } } func TestDocker_UnhealthyStepBlocksTheButton(t *testing.T) { f := newFix(t) f.dockerNight(t, "hp", true) f.dockerNight(t, "n100", true) f.now = f.now.Add(24 * time.Hour) f.dockerNight(t, "hp", false) f.dockerNight(t, "n100", true) if _, err := f.s.ApproveDocker(); err == nil || !strings.Contains(err.Error(), "health_failed") { t.Fatalf("an unhealthy Docker step did not block: %v", err) } } // Decision 157 (R-528): the Docker set is approved only when every ring-0 box's step showed the engine reports a // memory kill. RED-PROOF (REPORT): make oomCheckWaiting return "" — the three blocking tests below approve. func TestDockerApproval_AFailedMemoryKillCheckBlocks(t *testing.T) { f := newFix(t) fail := `{"result":"fail","oom_killed":false,"oom_event":true,"exit_code":137,"image":"felhom-controller","detail":"OOMKilled=false"}` for i := 0; i < 2; i++ { f.dockerNight(t, "hp", true) f.dockerNightOOM(t, "n100", true, fail) if i == 0 { f.s.Evaluate() // stamps the set\'s first-seen at the first night, as the tick does } f.now = f.now.Add(24 * time.Hour) } if _, err := f.s.ApproveDocker(); err == nil || !strings.Contains(err.Error(), "memory-kill check did not pass") { t.Fatalf("a set whose engine missed a memory kill was approvable: %v", err) } } func TestDockerApproval_AMissingMemoryKillCheckBlocks(t *testing.T) { f := newFix(t) for i := 0; i < 2; i++ { f.dockerNight(t, "hp", true) f.dockerNightOOM(t, "n100", true, "") // an older agent: no check at all if i == 0 { f.s.Evaluate() // stamps the set\'s first-seen at the first night, as the tick does } f.now = f.now.Add(24 * time.Hour) } if _, err := f.s.ApproveDocker(); err == nil || !strings.Contains(err.Error(), "has not reported the Docker step's memory-kill check") { t.Fatalf("a set with no memory-kill check was approvable: %v", err) } } func TestDockerApproval_AnErroredMemoryKillCheckBlocks(t *testing.T) { f := newFix(t) errd := `{"result":"error","oom_killed":false,"oom_event":false,"exit_code":null,"image":null,"detail":"the controller's image could not be read"}` for i := 0; i < 2; i++ { f.dockerNight(t, "hp", true) f.dockerNightOOM(t, "n100", true, errd) if i == 0 { f.s.Evaluate() // stamps the set\'s first-seen at the first night, as the tick does } f.now = f.now.Add(24 * time.Hour) } if _, err := f.s.ApproveDocker(); err == nil { t.Fatal("a set whose memory-kill check errored was approvable") } } func TestDockerApproval_APassingCheckOnEveryBoxAllows(t *testing.T) { f := newFix(t) for i := 0; i < 2; i++ { f.dockerNight(t, "hp", true) f.dockerNight(t, "n100", true) if i == 0 { f.s.Evaluate() // stamps the set\'s first-seen at the first night, as the tick does } f.now = f.now.Add(24 * time.Hour) } if _, err := f.s.ApproveDocker(); err != nil { t.Fatalf("passing checks on every ring-0 box should allow the approval: %v", err) } }