mesh/check failed: integration: github.com/novox/mesh-controller/internal/identity.TestACertificateChainsToTheMeshsOwnAuthority: authority_test.go:45:…
mesh/merge-gate head f501eaca4305 merged into trunk 4463991e0609 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head f501eaca4305 merged into trunk 4463991e0609 — pass: its merge-check.sh passed
mesh/delivery covered: a later merge that contains it was delivered: novox/mesh-controller@01f615aa0ad6 (merged as 8f2d22db into main, walk plan-17926707…
The registry push tags the image a second time, so removing only the build tag freed nothing. The held image a comparison pulls goes after it is read, and a node under the floor leaves the ask to another build node instead of taking it and failing.
217 lines
8.1 KiB
Go
217 lines
8.1 KiB
Go
package builder
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Issue 1834: a build node's root disk filled and every check there ended "the store never became ready". A node
|
|
// frees what each build and check leaves, refuses to take work under a floor of free space, and a store that does
|
|
// not come up says why.
|
|
|
|
func init() {
|
|
// The disk the tests run on is none of their business; the tests of the floor say their own.
|
|
FreeBytes = func(string) (uint64, error) { return 500 << 30, nil }
|
|
}
|
|
|
|
func withFree(t *testing.T, bytes uint64) {
|
|
t.Helper()
|
|
was := FreeBytes
|
|
FreeBytes = func(string) (uint64, error) { return bytes, nil }
|
|
t.Cleanup(func() { FreeBytes = was })
|
|
}
|
|
|
|
// 1834/AC2: given 3 GiB free, when a check or a build is asked, then it refuses at once, saying so, running nothing.
|
|
func TestACheckUnderTheFloorOfFreeSpaceRefusesSayingSo(t *testing.T) {
|
|
withFree(t, 3<<30)
|
|
var ran []string
|
|
run := func(_ context.Context, _ string, name string, args ...string) (string, error) {
|
|
ran = append(ran, name+" "+strings.Join(args, " "))
|
|
return "", nil
|
|
}
|
|
_, err := Check(context.Background(), run, CheckSpec{ID: "build-1"}, t.TempDir(), "registry.invalid", GitCredential{}, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "the build node has 3 GiB free on /, under the floor of 20 GiB") {
|
|
t.Fatalf("a check on a full disk said %v", err)
|
|
}
|
|
if len(ran) != 0 {
|
|
t.Errorf("a refused check ran %v", ran)
|
|
}
|
|
}
|
|
|
|
func TestABuildUnderTheFloorOfFreeSpaceRefusesSayingSo(t *testing.T) {
|
|
withFree(t, 3<<30)
|
|
r := &recorded{}
|
|
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "under the floor of 20 GiB") {
|
|
t.Fatalf("a build on a full disk said %v", err)
|
|
}
|
|
if len(r.ran) != 0 {
|
|
t.Errorf("a refused build ran %v", r.ran)
|
|
}
|
|
}
|
|
|
|
// The workspace's filesystem is read too, and a workspace not made yet is read at its nearest folder.
|
|
func TestTheWorkspacesFilesystemIsReadToo(t *testing.T) {
|
|
was := FreeBytes
|
|
t.Cleanup(func() { FreeBytes = was })
|
|
missing := filepath.Join(t.TempDir(), "not", "yet")
|
|
FreeBytes = func(p string) (uint64, error) {
|
|
if p == "/" {
|
|
return 100 << 30, nil
|
|
}
|
|
if _, err := os.Stat(p); err != nil {
|
|
t.Errorf("read the free space of %s, which is not there", p)
|
|
}
|
|
return 1 << 30, nil
|
|
}
|
|
err := RoomToWork("/", missing)
|
|
if err == nil || !strings.Contains(err.Error(), "1 GiB free on "+missing) {
|
|
t.Errorf("the workspace's disk, under the floor, said %v", err)
|
|
}
|
|
FreeBytes = func(string) (uint64, error) { return 0, errors.New("no") }
|
|
if RoomToWork("/") == nil {
|
|
t.Error("a disk that cannot be read was taken as having room")
|
|
}
|
|
}
|
|
|
|
// 1834/AC3: given a store that stops at once, when the check waits for it, then its exit code and last log lines are
|
|
// in the error and the container is removed afterwards, not by --rm before anyone could read it.
|
|
func TestAStoreThatDiesSaysWhyItDied(t *testing.T) {
|
|
var ran []string
|
|
run := func(_ context.Context, _ string, name string, args ...string) (string, error) {
|
|
line := name + " " + strings.Join(args, " ")
|
|
ran = append(ran, line)
|
|
switch {
|
|
case args[0] == "exec":
|
|
return "", errors.New("Error response from daemon: container is not running")
|
|
case args[0] == "inspect":
|
|
return "exited exit code 1\n", nil
|
|
case args[0] == "logs":
|
|
return "initdb: error: could not create directory: No space left on device\n", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
err := waitReady(context.Background(), run, "build-1-store", []string{"pg_isready"}, 1)
|
|
for _, want := range []string{"build-1-store never became ready", "exit code 1", "No space left on device"} {
|
|
if err == nil || !strings.Contains(err.Error(), want) {
|
|
t.Errorf("the verdict %q lacks %q", err, want)
|
|
}
|
|
}
|
|
logs := ""
|
|
for _, l := range ran {
|
|
if strings.HasPrefix(l, "docker logs") {
|
|
logs = l
|
|
}
|
|
}
|
|
if logs != "docker logs --tail 20 build-1-store" {
|
|
t.Errorf("the log was read as %q", logs)
|
|
}
|
|
}
|
|
|
|
func TestTheThrowawayStoreIsNotStartedWithRm(t *testing.T) {
|
|
var started []string
|
|
run := func(_ context.Context, _ string, name string, args ...string) (string, error) {
|
|
if args[0] == "run" {
|
|
started = args
|
|
}
|
|
return "127.0.0.1:40000\n", nil
|
|
}
|
|
if _, err := throwaway(context.Background(), run, run, "build-1-store", "postgres:17-alpine", 5432, nil, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, a := range started {
|
|
if a == "--rm" {
|
|
t.Errorf("started with --rm, which erases why it died: %v", started)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1834/AC1: given an image built for a check or a walk and published to the registry, when the build ends, then the
|
|
// local image tag, the narrowed source and the context clone it left are gone; an image published nowhere (a mesh
|
|
// being raised) is kept, since it is the artifact.
|
|
// pushedNamed is a publisher that tags the image as <registry>/<repository> to push it, as the registry one does.
|
|
type pushedNamed struct{ *recorded }
|
|
|
|
func (pushedNamed) PushedNames(repository string) []string {
|
|
return []string{"registry.invalid/" + repository}
|
|
}
|
|
|
|
func TestABuildRemovesWhatItLeftOnTheNode(t *testing.T) {
|
|
r := &recorded{
|
|
contents: map[string]string{"modules/m/" + ManifestName: anImageWithAContext, "modules/m/Dockerfile": "FROM scratch\n"},
|
|
secondary: map[string]map[string]string{"https://forge.invalid/ctl.git": {"go.mod": "module x\n"}},
|
|
}
|
|
workspace := t.TempDir()
|
|
if _, err := Build(context.Background(), r.run, pushedNamed{r}, "https://forge.invalid/c.git", "modules/m", "", workspace, nil, Npmrc{},
|
|
GitCredential{}, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Both names go in one rmi: the layers stay while either is left (the push tagged the image with the registry's).
|
|
if !strings.Contains(strings.Join(r.ran, "\n"), "docker rmi m-server:c0ffeec0 registry.invalid/m/server") {
|
|
t.Errorf("the image built for it and its pushed name were not removed: %v", r.ran)
|
|
}
|
|
left, _ := filepath.Glob(filepath.Join(workspace, "*"))
|
|
if len(left) != 0 {
|
|
t.Errorf("the build left %v in its workspace", left)
|
|
}
|
|
}
|
|
|
|
const anImageWithAContext = `{"module":"m","version":"1",
|
|
"build":{"artifacts":[{"name":"server","kind":"image","from":"Dockerfile",
|
|
"context":{"repository":"https://forge.invalid/ctl.git","ref":"main"}}]}}`
|
|
|
|
func TestAnImageKeptLocallyIsNotRemoved(t *testing.T) {
|
|
r := &recorded{contents: map[string]string{"modules/m/" + ManifestName: anImageWithAContext, "modules/m/Dockerfile": "FROM scratch\n"}}
|
|
local := Local{Run: func(ctx context.Context, dir, name string, args ...string) (string, error) {
|
|
if args[0] == "image" {
|
|
return "sha256:" + strings.Repeat("a", 64) + "\n", nil
|
|
}
|
|
return r.run(ctx, dir, name, args...)
|
|
}}
|
|
if _, err := Build(context.Background(), local.Run, local, "https://forge.invalid/c.git", "modules/m", "", t.TempDir(), nil, Npmrc{},
|
|
GitCredential{}, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(strings.Join(r.ran, "\n"), "docker rmi") {
|
|
t.Errorf("the artifact of a mesh being raised was removed: %v", r.ran)
|
|
}
|
|
}
|
|
|
|
// The held image the comparison pulls to read is let go of once read.
|
|
func TestTheHeldImageAComparisonPullsIsRemovedAfterReading(t *testing.T) {
|
|
var ran []string
|
|
run := func(_ context.Context, _ string, name string, args ...string) (string, error) {
|
|
ran = append(ran, name+" "+strings.Join(args, " "))
|
|
if args[0] == "create" {
|
|
return "abcdef0123456789\n", nil
|
|
}
|
|
if args[0] == "image" {
|
|
return "", errors.New("stop here")
|
|
}
|
|
return "", nil
|
|
}
|
|
_, _ = imageContent(context.Background(), run, t.TempDir(), "reg.invalid/m/server@sha256:"+strings.Repeat("a", 64))
|
|
if got := ran[len(ran)-1]; !strings.HasPrefix(got, "docker rmi reg.invalid/m/server@sha256:") {
|
|
t.Errorf("the last command was %q, not the removal of the pulled image: %v", got, ran)
|
|
}
|
|
}
|
|
|
|
func TestAnImageThatCannotBeRemovedIsNotSaidRemoved(t *testing.T) {
|
|
var said []string
|
|
run := func(context.Context, string, string, ...string) (string, error) {
|
|
return "", errors.New("image is being used")
|
|
}
|
|
releaseImage(context.Background(), run, []string{"a:1"}, func(step, format string, args ...any) {
|
|
said = append(said, format)
|
|
})
|
|
for _, s := range said {
|
|
if strings.Contains(s, "is removed") {
|
|
t.Errorf("said removed: %q", s)
|
|
}
|
|
}
|
|
}
|