126 lines
5.3 KiB
Go
126 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
)
|
|
|
|
// **No declaration is sent that names an image the artifact store does not hold** (novox/hq issue 1742).
|
|
|
|
const storeAt = "novox.internal:5100"
|
|
|
|
// automx is the image the send of 2026-10-21 17:13 named and the store lacked; the others are held.
|
|
const (
|
|
automxImage = storeAt + "/mailu/automx@sha256:c62150b8e2cbcc18cb911b098c2877f928761a1e6251667f39ea18251ea35591"
|
|
imapImage = storeAt + "/mailu/imap@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
|
)
|
|
|
|
func aMailSend() sendable {
|
|
return sendable{
|
|
Resources: []map[string]any{
|
|
{"id": "mailu.automx", "type": "container", "image": automxImage},
|
|
{"id": "mailu.automx-config", "type": "file", "path": "/etc/automx.conf"},
|
|
{"id": "mailu.imap", "type": "container", "image": imapImage},
|
|
{"id": "other.web", "type": "container", "image": "docker.io/library/nginx@sha256:" + strings.Repeat("b", 64)},
|
|
{"id": "dns.dnsmasq", "type": "container", "image": storeAt + "/dnsmasq/main@sha256:" + strings.Repeat("c", 64)},
|
|
},
|
|
moduleOf: map[string]string{"mailu.automx": "automx", "mailu.automx-config": "automx", "mailu.imap": "mailu",
|
|
"other.web": "other", "dns.dnsmasq": "dnsmasq"},
|
|
Builds: map[string]string{"automx": "b2f5ebb2new", "mailu": "m2", "other": "o1", "dnsmasq": "d1"},
|
|
}
|
|
}
|
|
|
|
func storeHolding(missing ...string) (storeHolds, *[]string) {
|
|
var asked []string
|
|
return func(_ context.Context, reference string) (bool, error) {
|
|
asked = append(asked, reference)
|
|
for _, m := range missing {
|
|
if strings.Contains(reference, m) {
|
|
return false, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}, &asked
|
|
}
|
|
|
|
// 1742/AC1: given a send naming automx's image, which the store does not hold, when the declaration is judged, then
|
|
// every resource of automx is out of it, automx is among the modules left out with the reason, its build is the one the
|
|
// machine was last sent, and every other module — an image of another registry among them — is as composed.
|
|
func TestAModuleWhoseImageTheStoreLacksIsLeftOutOfTheSend(t *testing.T) {
|
|
s := aMailSend()
|
|
holds, asked := storeHolding("mailu/automx@")
|
|
found := holdBackMissingImages(t.Context(), "novox", &s, storeAt, holds, map[string]string{"automx": "b2f5ebb2old"})
|
|
if len(found) != 1 || found[0].Module != "automx" || found[0].Node != "novox" || found[0].Image != automxImage {
|
|
t.Fatalf("found %+v", found)
|
|
}
|
|
var ids []string
|
|
for _, r := range s.Resources {
|
|
ids = append(ids, r["id"].(string))
|
|
}
|
|
if got := strings.Join(ids, ","); got != "mailu.imap,other.web,dns.dnsmasq" {
|
|
t.Errorf("the declaration keeps %s", got)
|
|
}
|
|
if len(s.LeftOut) != 1 || s.LeftOut[0] != "automx" || !strings.Contains(s.leftOutWhy["automx"], "not in the artifact store") {
|
|
t.Errorf("left out %v because %v", s.LeftOut, s.leftOutWhy)
|
|
}
|
|
if s.Builds["automx"] != "b2f5ebb2old" || s.Builds["mailu"] != "m2" || s.Builds["other"] != "o1" {
|
|
t.Errorf("builds %v", s.Builds)
|
|
}
|
|
for _, a := range *asked {
|
|
if strings.Contains(a, "docker.io") {
|
|
t.Errorf("the store was asked about an image of another registry: %s", a)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1742/AC2: given a module never sent to the machine, when its image is missing, then it has no build in the record of the
|
|
// send; given the store does not answer, then nothing is held back for a question unanswered.
|
|
func TestAnUnansweredStoreHoldsNothingBackAndANewModuleHasNoBuild(t *testing.T) {
|
|
s := aMailSend()
|
|
holds, _ := storeHolding("mailu/automx@")
|
|
found := holdBackMissingImages(t.Context(), "novox", &s, storeAt, holds, nil)
|
|
if len(found) != 1 {
|
|
t.Fatalf("%+v", found)
|
|
}
|
|
if _, has := s.Builds["automx"]; has {
|
|
t.Errorf("a module never sent is recorded as sent: %v", s.Builds)
|
|
}
|
|
s = aMailSend()
|
|
silent := func(context.Context, string) (bool, error) { return false, errors.New("registry: connection refused") }
|
|
if found := holdBackMissingImages(t.Context(), "novox", &s, storeAt, silent, nil); len(found) != 0 || len(s.LeftOut) != 0 ||
|
|
len(s.Resources) != 5 {
|
|
t.Errorf("an unanswered store held back %+v, left out %v", found, s.LeftOut)
|
|
}
|
|
}
|
|
|
|
// 1742/AC3: a module left out for its image raises a condition for the operator naming the module, the machine and the
|
|
// image in the evidence, plain words, and no answer to give; a mesh whose images are all held raises nothing.
|
|
func TestAMissingImageIsRaisedForTheOperator(t *testing.T) {
|
|
f := calm(time.Date(2026, 10, 21, 17, 40, 0, 0, time.UTC))
|
|
if got := watchMissingImages(f); len(got) != 0 {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
f.images = []imageFacts{{missingImage{Node: "novox", Module: "automx", Image: automxImage}}}
|
|
got := watchMissingImages(f)
|
|
if len(got) != 1 {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
o := got[0]
|
|
if o.Kind != "image-missing" || o.Key() != "module.automx.novox.image-missing" || o.Resolver != conditions.ResolverOperator ||
|
|
o.Severity != conditions.Warning || o.Machine != "novox" {
|
|
t.Fatalf("%+v (%s)", o, o.Key())
|
|
}
|
|
if !strings.Contains(o.Said, "c62150b8") || !strings.Contains(o.Headline+o.Explanation, "automx") {
|
|
t.Errorf("words: %q / %q / %q", o.Headline, o.Explanation, o.Said)
|
|
}
|
|
w := conditions.Words{Headline: o.Headline, Explanation: o.Explanation, Resolved: o.Resolved, Needs: o.Needs}
|
|
if why, ok := conditions.PlainWords(w, "novox"); !ok {
|
|
t.Errorf("not plain: %s", why)
|
|
}
|
|
}
|