Split the unit level from the integration level in cmd, builder, link, catalogue, inventory; cheap fixtures for the archive-entry bound, the registry waits and the catalogue copy (issue 895)
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
func TestABuildProducesAManifestThePinsAreIn(t *testing.T) {
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{
|
||||
"Dockerfile": "FROM scratch", "files/theme.conf": "dark",
|
||||
})
|
||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Commit != "c0ffeec0ffeec0ffeec0ffeec0ffeec0ffeec0ff" {
|
||||
t.Fatalf("the commit was not recorded: %q", got.Commit)
|
||||
}
|
||||
if got.Manifest.Resources[0]["image"] == nil {
|
||||
t.Fatalf("the image was not pinned: %v", got.Manifest.Resources[0])
|
||||
}
|
||||
digest, _ := got.Manifest.Resources[1]["digest"].(string)
|
||||
if !strings.HasPrefix(digest, "sha256:") {
|
||||
t.Fatalf("the archive was not pinned: %v", got.Manifest.Resources[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoBuildsOfOneCommitProduceOneDigest(t *testing.T) {
|
||||
// Or nothing downstream can tell "this changed" from "this was built again", and every
|
||||
// rebuild looks like a change to every machine holding it.
|
||||
var digests []string
|
||||
for i := 0; i < 2; i++ {
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{
|
||||
"Dockerfile": "FROM scratch", "files/a.conf": "one", "files/b.conf": "two",
|
||||
})
|
||||
// A year apart, so a packer carrying timestamps cannot accidentally agree.
|
||||
r.stamped = time.Date(2020+i, time.March, 3, 4, 5, 6, 0, time.UTC)
|
||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range got.Built {
|
||||
if b.Kind == catalogue.ArtifactArchive {
|
||||
digests = append(digests, b.Digest)
|
||||
}
|
||||
}
|
||||
}
|
||||
if digests[0] != digests[1] {
|
||||
t.Fatalf("two builds of one commit produced %s and %s", digests[0], digests[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNothingIsPublishedUntilEverythingIsBuilt(t *testing.T) {
|
||||
// Half a module in the store under a digest the mesh never records is reachable,
|
||||
// unreferenced, and indistinguishable from something in use.
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch"})
|
||||
// `files` is missing, so packing the archive fails — after the image would have been pushed.
|
||||
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a build with a missing input succeeded")
|
||||
}
|
||||
if len(r.archives) != 0 {
|
||||
t.Fatalf("an archive was published by a failed build: %v", r.archives)
|
||||
}
|
||||
}
|
||||
|
||||
func TestARepositoryWithNoManifestSaysSo(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
r := &recorded{contents: map[string]string{"README.md": "nothing to see"}}
|
||||
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a repository with nothing saying what it is was built")
|
||||
}
|
||||
if !strings.Contains(err.Error(), ManifestName) {
|
||||
t.Fatalf("the failure does not name what is missing: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAModuleThatBuildsNothingStillProducesAManifest(t *testing.T) {
|
||||
// Most of what a person installs is configuration.
|
||||
r, workspace := aRepository(t, `{"module":"shell","version":"1","resources":[
|
||||
{"id":"rc","type":"file","path":"/etc/zsh/zshrc","content":"setopt"}]}`, nil)
|
||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/shell.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got.Built) != 0 {
|
||||
t.Fatalf("something was built: %v", got.Built)
|
||||
}
|
||||
if got.Manifest.Module != "shell" || len(got.Manifest.Resources) != 1 {
|
||||
t.Fatalf("got %+v", got.Manifest)
|
||||
}
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker") {
|
||||
t.Fatalf("docker was run for a module that builds nothing: %q", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheTreeIsFreshEveryTime(t *testing.T) {
|
||||
// A build that reuses a working tree can succeed because of something a previous build left
|
||||
// behind, and that is a build nobody can reproduce.
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{
|
||||
"Dockerfile": "FROM scratch", "files/a": "b",
|
||||
})
|
||||
leftover := filepath.Join(workspace, "source", "files", "from-last-time")
|
||||
if err := os.MkdirAll(filepath.Dir(leftover), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(leftover, []byte("stale"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(leftover); err == nil {
|
||||
t.Fatal("a previous build's file survived into this one")
|
||||
}
|
||||
}
|
||||
|
||||
func TestABuildThatCannotPushFails(t *testing.T) {
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{
|
||||
"Dockerfile": "FROM scratch", "files/a": "b",
|
||||
})
|
||||
r.failPush = true
|
||||
if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil); err == nil {
|
||||
t.Fatal("a build that could publish nothing reported success")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUpstreamImageIsMirroredRatherThanBuilt(t *testing.T) {
|
||||
// A module usually runs software it did not write. Naming the upstream reference directly
|
||||
// would need every machine to reach a public registry, and would pin to a tag somebody else
|
||||
// can move.
|
||||
const mirrors = `{"module":"postgres","version":"1",
|
||||
"build":{"artifacts":[{"name":"store","kind":"upstream","from":"postgres:17-alpine"}]},
|
||||
"resources":[{"id":"db","type":"container","name":"mesh-postgres","artifact":"store"}]}`
|
||||
|
||||
r, workspace := aRepository(t, mirrors, nil)
|
||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/postgres.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Pulled, not built.
|
||||
var pulled, built bool
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker pull postgres:17-alpine") {
|
||||
pulled = true
|
||||
}
|
||||
if strings.HasPrefix(line, "docker build") {
|
||||
built = true
|
||||
}
|
||||
}
|
||||
if !pulled {
|
||||
t.Fatalf("the upstream image was not fetched: %v", r.ran)
|
||||
}
|
||||
if built {
|
||||
t.Fatalf("something was built for an image that is mirrored: %v", r.ran)
|
||||
}
|
||||
// And the resource names what this registry serves, pinned by the digest it assigned.
|
||||
image, _ := got.Manifest.Resources[0]["image"].(string)
|
||||
if !strings.Contains(image, "@sha256:") {
|
||||
t.Fatalf("the mirrored image is not pinned by digest: %q", image)
|
||||
}
|
||||
if strings.Contains(image, "17-alpine") {
|
||||
t.Fatalf("the resource still names the upstream tag: %q", image)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAModuleIsBuiltFromItsPathWithinTheRepository(t *testing.T) {
|
||||
r := &recorded{contents: map[string]string{
|
||||
"README.md": "this repository holds several modules",
|
||||
"modules/shell/" + ManifestName: withBoth,
|
||||
"modules/shell/Dockerfile": "FROM scratch",
|
||||
"modules/shell/files/theme.conf": "dark",
|
||||
// A second module beside it, so what is built is chosen by the path rather than by
|
||||
// happening to be the only manifest in the clone.
|
||||
"modules/other/" + ManifestName: `{"module":"other","version":"1"}`,
|
||||
}}
|
||||
got, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/catalogue.git", "modules/shell", "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Manifest.Module != "meshboard" {
|
||||
t.Fatalf("built %q, which is not the module at the path asked for", got.Manifest.Module)
|
||||
}
|
||||
if got.Manifest.Resources[0]["image"] == nil {
|
||||
t.Fatalf("the image was not pinned: %v", got.Manifest.Resources[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPathThatLeavesTheRepositoryIsRefused(t *testing.T) {
|
||||
for _, escaping := range []string{"../../etc", "/etc"} {
|
||||
r := &recorded{contents: map[string]string{ManifestName: withBoth}}
|
||||
_, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/x.git", escaping, "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatalf("%q was accepted as a module's path", escaping)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "leaves the repository") &&
|
||||
!strings.Contains(err.Error(), "absolute path") {
|
||||
t.Fatalf("the refusal of %q does not say why: %v", escaping, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnArtifactWithItsOwnContextIsBuiltFromThere(t *testing.T) {
|
||||
const withContext = `{"module":"route-proxy","version":"1",
|
||||
"build":{"artifacts":[
|
||||
{"name":"server","kind":"image","from":"Dockerfile",
|
||||
"context":{"repository":"https://forge.invalid/source.git","ref":"main"}}]}}`
|
||||
r := &recorded{
|
||||
contents: map[string]string{
|
||||
ManifestName: withContext,
|
||||
// The recipe lives with the packaging, not the source — read from here regardless of
|
||||
// where the build context comes from. FROM scratch declares no base, so what is under
|
||||
// test — where the context comes from — is not entangled with ADR 0097's own checks.
|
||||
"Dockerfile": "FROM scratch\nCOPY go.mod ./\n",
|
||||
},
|
||||
secondary: map[string]map[string]string{
|
||||
// go.mod exists only in the second repository. A build context taken from the wrong
|
||||
// place would never find it, which a real docker build would refuse on — the fake
|
||||
// does not read files, so what is checked below is that the build was even pointed
|
||||
// at the right place, not that COPY would have succeeded.
|
||||
"https://forge.invalid/source.git": {"go.mod": "module route-proxy\n"},
|
||||
},
|
||||
}
|
||||
_, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/catalogue.git", "", "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var clonedSource bool
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "git clone") && strings.Contains(line, "https://forge.invalid/source.git") {
|
||||
clonedSource = true
|
||||
}
|
||||
}
|
||||
if !clonedSource {
|
||||
t.Fatalf("the artifact's own context was never cloned: %v", r.ran)
|
||||
}
|
||||
|
||||
buildIndex := -1
|
||||
for i, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker build ") {
|
||||
buildIndex = i
|
||||
}
|
||||
}
|
||||
if buildIndex == -1 {
|
||||
t.Fatal("no docker build was run")
|
||||
}
|
||||
build := r.ran[buildIndex]
|
||||
buildDir := r.dirs[buildIndex]
|
||||
|
||||
if !strings.Contains(buildDir, "context-server") {
|
||||
t.Errorf("docker build ran from %q, not the artifact's own cloned context", buildDir)
|
||||
}
|
||||
recipe := strings.SplitN(strings.SplitN(build, "-f ", 2)[1], " ", 2)[0]
|
||||
if !filepath.IsAbs(recipe) {
|
||||
t.Errorf("the recipe %q is not an absolute path, so it is read relative to whatever "+
|
||||
"directory the build context moved to rather than where it actually is", recipe)
|
||||
}
|
||||
if !strings.HasSuffix(recipe, string(filepath.Separator)+"Dockerfile") {
|
||||
t.Errorf("the recipe is not the module's own Dockerfile: %q", recipe)
|
||||
}
|
||||
if !strings.HasSuffix(build, " .") {
|
||||
t.Errorf("the build was not given a context: %s", build)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABuildOffersTheForgesCredentialThroughGitsOwnStore(t *testing.T) {
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{
|
||||
"Dockerfile": "FROM scratch", "files/theme.conf": "dark",
|
||||
})
|
||||
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "",
|
||||
workspace, nil, Npmrc{},
|
||||
GitCredential{URL: "http://mesh_novox_builder:sw0rdfi5h@forge.invalid:20000"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clone := r.ran[0]
|
||||
stored := storeNamedIn(t, clone)
|
||||
for _, line := range r.ran {
|
||||
if strings.Contains(line, "sw0rdfi5h") {
|
||||
t.Fatalf("the secret is in a command line, readable by anything that can list processes: %s", line)
|
||||
}
|
||||
}
|
||||
// Outside the workspace a check's container mounts, and gone once the build is (novox/hq issue 462); what
|
||||
// it held while git read it is checked with the check's clones (TestACheckContainerSeesNoForgeCredential).
|
||||
if withinDir(workspace, stored) {
|
||||
t.Fatalf("the credential store %s is inside the workspace %s", stored, workspace)
|
||||
}
|
||||
if _, err := os.Stat(stored); !os.IsNotExist(err) {
|
||||
t.Fatalf("the credential store outlives the build: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(workspace, "git-credentials")); !os.IsNotExist(err) {
|
||||
t.Fatal("a credential file is left in the workspace")
|
||||
}
|
||||
}
|
||||
|
||||
func TestABuildWithNoCredentialClonesExactlyAsBefore(t *testing.T) {
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{
|
||||
"Dockerfile": "FROM scratch", "files/theme.conf": "dark",
|
||||
})
|
||||
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "",
|
||||
workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(r.ran[0], "git clone --quiet ") {
|
||||
t.Fatalf("a credential-less clone grew flags: %s", r.ran[0])
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(workspace, "git-credentials")); !os.IsNotExist(err) {
|
||||
t.Fatal("a credential file was written with no credential to put in it")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAContextCloneCarriesTheSameCredentialStore(t *testing.T) {
|
||||
const withContext = `{"module":"route-proxy","version":"1",
|
||||
"build":{"artifacts":[
|
||||
{"name":"server","kind":"image","from":"Dockerfile",
|
||||
"context":{"repository":"https://forge.invalid/source.git","ref":"main"}}]}}`
|
||||
r := &recorded{
|
||||
contents: map[string]string{
|
||||
ManifestName: withContext,
|
||||
"Dockerfile": "FROM scratch\nCOPY go.mod ./\n",
|
||||
},
|
||||
secondary: map[string]map[string]string{
|
||||
"https://forge.invalid/source.git": {"go.mod": "module route-proxy\n"},
|
||||
},
|
||||
}
|
||||
workspace := t.TempDir()
|
||||
_, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/catalogue.git", "", "", workspace, nil, Npmrc{},
|
||||
GitCredential{URL: "https://builder:s3cret@forge.invalid"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored := storeNamedIn(t, r.ran[0])
|
||||
var contextClone string
|
||||
for _, line := range r.ran {
|
||||
if strings.Contains(line, "clone") && strings.Contains(line, "source.git") {
|
||||
contextClone = line
|
||||
}
|
||||
}
|
||||
if contextClone == "" {
|
||||
t.Fatalf("the context was never cloned: %v", r.ran)
|
||||
}
|
||||
if !strings.Contains(contextClone, "credential.helper=store --file="+stored) {
|
||||
t.Fatalf("the context clone does not name the credential store: %s", contextClone)
|
||||
}
|
||||
}
|
||||
@@ -130,168 +130,6 @@ const withBoth = `{"module":"meshboard","version":"1",
|
||||
{"id":"svc","type":"container","name":"meshboard","artifact":"server"},
|
||||
{"id":"theme","type":"archive","path":"/opt/meshboard","artifact":"look"}]}`
|
||||
|
||||
func TestABuildProducesAManifestThePinsAreIn(t *testing.T) {
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{
|
||||
"Dockerfile": "FROM scratch", "files/theme.conf": "dark",
|
||||
})
|
||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Commit != "c0ffeec0ffeec0ffeec0ffeec0ffeec0ffeec0ff" {
|
||||
t.Fatalf("the commit was not recorded: %q", got.Commit)
|
||||
}
|
||||
if got.Manifest.Resources[0]["image"] == nil {
|
||||
t.Fatalf("the image was not pinned: %v", got.Manifest.Resources[0])
|
||||
}
|
||||
digest, _ := got.Manifest.Resources[1]["digest"].(string)
|
||||
if !strings.HasPrefix(digest, "sha256:") {
|
||||
t.Fatalf("the archive was not pinned: %v", got.Manifest.Resources[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoBuildsOfOneCommitProduceOneDigest(t *testing.T) {
|
||||
// Or nothing downstream can tell "this changed" from "this was built again", and every
|
||||
// rebuild looks like a change to every machine holding it.
|
||||
var digests []string
|
||||
for i := 0; i < 2; i++ {
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{
|
||||
"Dockerfile": "FROM scratch", "files/a.conf": "one", "files/b.conf": "two",
|
||||
})
|
||||
// A year apart, so a packer carrying timestamps cannot accidentally agree.
|
||||
r.stamped = time.Date(2020+i, time.March, 3, 4, 5, 6, 0, time.UTC)
|
||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, b := range got.Built {
|
||||
if b.Kind == catalogue.ArtifactArchive {
|
||||
digests = append(digests, b.Digest)
|
||||
}
|
||||
}
|
||||
}
|
||||
if digests[0] != digests[1] {
|
||||
t.Fatalf("two builds of one commit produced %s and %s", digests[0], digests[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestNothingIsPublishedUntilEverythingIsBuilt(t *testing.T) {
|
||||
// Half a module in the store under a digest the mesh never records is reachable,
|
||||
// unreferenced, and indistinguishable from something in use.
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch"})
|
||||
// `files` is missing, so packing the archive fails — after the image would have been pushed.
|
||||
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a build with a missing input succeeded")
|
||||
}
|
||||
if len(r.archives) != 0 {
|
||||
t.Fatalf("an archive was published by a failed build: %v", r.archives)
|
||||
}
|
||||
}
|
||||
|
||||
func TestARepositoryWithNoManifestSaysSo(t *testing.T) {
|
||||
workspace := t.TempDir()
|
||||
r := &recorded{contents: map[string]string{"README.md": "nothing to see"}}
|
||||
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a repository with nothing saying what it is was built")
|
||||
}
|
||||
if !strings.Contains(err.Error(), ManifestName) {
|
||||
t.Fatalf("the failure does not name what is missing: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAModuleThatBuildsNothingStillProducesAManifest(t *testing.T) {
|
||||
// Most of what a person installs is configuration.
|
||||
r, workspace := aRepository(t, `{"module":"shell","version":"1","resources":[
|
||||
{"id":"rc","type":"file","path":"/etc/zsh/zshrc","content":"setopt"}]}`, nil)
|
||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/shell.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got.Built) != 0 {
|
||||
t.Fatalf("something was built: %v", got.Built)
|
||||
}
|
||||
if got.Manifest.Module != "shell" || len(got.Manifest.Resources) != 1 {
|
||||
t.Fatalf("got %+v", got.Manifest)
|
||||
}
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker") {
|
||||
t.Fatalf("docker was run for a module that builds nothing: %q", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheTreeIsFreshEveryTime(t *testing.T) {
|
||||
// A build that reuses a working tree can succeed because of something a previous build left
|
||||
// behind, and that is a build nobody can reproduce.
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{
|
||||
"Dockerfile": "FROM scratch", "files/a": "b",
|
||||
})
|
||||
leftover := filepath.Join(workspace, "source", "files", "from-last-time")
|
||||
if err := os.MkdirAll(filepath.Dir(leftover), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(leftover, []byte("stale"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := os.Stat(leftover); err == nil {
|
||||
t.Fatal("a previous build's file survived into this one")
|
||||
}
|
||||
}
|
||||
|
||||
func TestABuildThatCannotPushFails(t *testing.T) {
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{
|
||||
"Dockerfile": "FROM scratch", "files/a": "b",
|
||||
})
|
||||
r.failPush = true
|
||||
if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/x.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil); err == nil {
|
||||
t.Fatal("a build that could publish nothing reported success")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUpstreamImageIsMirroredRatherThanBuilt(t *testing.T) {
|
||||
// A module usually runs software it did not write. Naming the upstream reference directly
|
||||
// would need every machine to reach a public registry, and would pin to a tag somebody else
|
||||
// can move.
|
||||
const mirrors = `{"module":"postgres","version":"1",
|
||||
"build":{"artifacts":[{"name":"store","kind":"upstream","from":"postgres:17-alpine"}]},
|
||||
"resources":[{"id":"db","type":"container","name":"mesh-postgres","artifact":"store"}]}`
|
||||
|
||||
r, workspace := aRepository(t, mirrors, nil)
|
||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/postgres.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Pulled, not built.
|
||||
var pulled, built bool
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker pull postgres:17-alpine") {
|
||||
pulled = true
|
||||
}
|
||||
if strings.HasPrefix(line, "docker build") {
|
||||
built = true
|
||||
}
|
||||
}
|
||||
if !pulled {
|
||||
t.Fatalf("the upstream image was not fetched: %v", r.ran)
|
||||
}
|
||||
if built {
|
||||
t.Fatalf("something was built for an image that is mirrored: %v", r.ran)
|
||||
}
|
||||
// And the resource names what this registry serves, pinned by the digest it assigned.
|
||||
image, _ := got.Manifest.Resources[0]["image"].(string)
|
||||
if !strings.Contains(image, "@sha256:") {
|
||||
t.Fatalf("the mirrored image is not pinned by digest: %q", image)
|
||||
}
|
||||
if strings.Contains(image, "17-alpine") {
|
||||
t.Fatalf("the resource still names the upstream tag: %q", image)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUpstreamImageWithNoTagIsRefused(t *testing.T) {
|
||||
// What gets mirrored would be whatever `latest` means today, and a module pinned to that is
|
||||
// not pinned.
|
||||
@@ -317,148 +155,19 @@ func TestAnUpstreamReferenceIsNotAPathInTheRepository(t *testing.T) {
|
||||
// **A module is a repository and a path within it** (novox/hq ADR 0069). The catalogue holds its
|
||||
// modules one to a directory and the system this replaces has always built one that way, so a
|
||||
// builder that could only read a repository's root could build none of what exists.
|
||||
func TestAModuleIsBuiltFromItsPathWithinTheRepository(t *testing.T) {
|
||||
r := &recorded{contents: map[string]string{
|
||||
"README.md": "this repository holds several modules",
|
||||
"modules/shell/" + ManifestName: withBoth,
|
||||
"modules/shell/Dockerfile": "FROM scratch",
|
||||
"modules/shell/files/theme.conf": "dark",
|
||||
// A second module beside it, so what is built is chosen by the path rather than by
|
||||
// happening to be the only manifest in the clone.
|
||||
"modules/other/" + ManifestName: `{"module":"other","version":"1"}`,
|
||||
}}
|
||||
got, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/catalogue.git", "modules/shell", "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Manifest.Module != "meshboard" {
|
||||
t.Fatalf("built %q, which is not the module at the path asked for", got.Manifest.Module)
|
||||
}
|
||||
if got.Manifest.Resources[0]["image"] == nil {
|
||||
t.Fatalf("the image was not pinned: %v", got.Manifest.Resources[0])
|
||||
}
|
||||
}
|
||||
|
||||
// A build reads only its own tree. A path climbing out of the clone would otherwise let a build
|
||||
// read — and an archive artifact publish — whatever the build machine happens to hold, which is
|
||||
// the one thing a machine that builds other people's repositories must not do.
|
||||
func TestAPathThatLeavesTheRepositoryIsRefused(t *testing.T) {
|
||||
for _, escaping := range []string{"../../etc", "/etc"} {
|
||||
r := &recorded{contents: map[string]string{ManifestName: withBoth}}
|
||||
_, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/x.git", escaping, "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatalf("%q was accepted as a module's path", escaping)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "leaves the repository") &&
|
||||
!strings.Contains(err.Error(), "absolute path") {
|
||||
t.Fatalf("the refusal of %q does not say why: %v", escaping, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **Packaging and source are allowed to live apart** — a module that ships only the recipe for
|
||||
// source that lives in a second repository (the reference route-proxy, packaged in the catalogue
|
||||
// but built from mesh-controller's own repository) names where that source actually is, rather
|
||||
// than vendoring a second copy the two could drift from.
|
||||
func TestAnArtifactWithItsOwnContextIsBuiltFromThere(t *testing.T) {
|
||||
const withContext = `{"module":"route-proxy","version":"1",
|
||||
"build":{"artifacts":[
|
||||
{"name":"server","kind":"image","from":"Dockerfile",
|
||||
"context":{"repository":"https://forge.invalid/source.git","ref":"main"}}]}}`
|
||||
r := &recorded{
|
||||
contents: map[string]string{
|
||||
ManifestName: withContext,
|
||||
// The recipe lives with the packaging, not the source — read from here regardless of
|
||||
// where the build context comes from. FROM scratch declares no base, so what is under
|
||||
// test — where the context comes from — is not entangled with ADR 0097's own checks.
|
||||
"Dockerfile": "FROM scratch\nCOPY go.mod ./\n",
|
||||
},
|
||||
secondary: map[string]map[string]string{
|
||||
// go.mod exists only in the second repository. A build context taken from the wrong
|
||||
// place would never find it, which a real docker build would refuse on — the fake
|
||||
// does not read files, so what is checked below is that the build was even pointed
|
||||
// at the right place, not that COPY would have succeeded.
|
||||
"https://forge.invalid/source.git": {"go.mod": "module route-proxy\n"},
|
||||
},
|
||||
}
|
||||
_, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/catalogue.git", "", "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var clonedSource bool
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "git clone") && strings.Contains(line, "https://forge.invalid/source.git") {
|
||||
clonedSource = true
|
||||
}
|
||||
}
|
||||
if !clonedSource {
|
||||
t.Fatalf("the artifact's own context was never cloned: %v", r.ran)
|
||||
}
|
||||
|
||||
buildIndex := -1
|
||||
for i, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker build ") {
|
||||
buildIndex = i
|
||||
}
|
||||
}
|
||||
if buildIndex == -1 {
|
||||
t.Fatal("no docker build was run")
|
||||
}
|
||||
build := r.ran[buildIndex]
|
||||
buildDir := r.dirs[buildIndex]
|
||||
|
||||
if !strings.Contains(buildDir, "context-server") {
|
||||
t.Errorf("docker build ran from %q, not the artifact's own cloned context", buildDir)
|
||||
}
|
||||
recipe := strings.SplitN(strings.SplitN(build, "-f ", 2)[1], " ", 2)[0]
|
||||
if !filepath.IsAbs(recipe) {
|
||||
t.Errorf("the recipe %q is not an absolute path, so it is read relative to whatever "+
|
||||
"directory the build context moved to rather than where it actually is", recipe)
|
||||
}
|
||||
if !strings.HasSuffix(recipe, string(filepath.Separator)+"Dockerfile") {
|
||||
t.Errorf("the recipe is not the module's own Dockerfile: %q", recipe)
|
||||
}
|
||||
if !strings.HasSuffix(build, " .") {
|
||||
t.Errorf("the build was not given a context: %s", build)
|
||||
}
|
||||
}
|
||||
|
||||
// The forge credential is offered through git's own credential store — a file, never argv — and
|
||||
// git decides when it applies. What is checked: the clone names the store, the secret never
|
||||
// appears in a command line, and the file holds exactly the URL at 0600.
|
||||
func TestABuildOffersTheForgesCredentialThroughGitsOwnStore(t *testing.T) {
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{
|
||||
"Dockerfile": "FROM scratch", "files/theme.conf": "dark",
|
||||
})
|
||||
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "",
|
||||
workspace, nil, Npmrc{},
|
||||
GitCredential{URL: "http://mesh_novox_builder:sw0rdfi5h@forge.invalid:20000"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clone := r.ran[0]
|
||||
stored := storeNamedIn(t, clone)
|
||||
for _, line := range r.ran {
|
||||
if strings.Contains(line, "sw0rdfi5h") {
|
||||
t.Fatalf("the secret is in a command line, readable by anything that can list processes: %s", line)
|
||||
}
|
||||
}
|
||||
// Outside the workspace a check's container mounts, and gone once the build is (novox/hq issue 462); what
|
||||
// it held while git read it is checked with the check's clones (TestACheckContainerSeesNoForgeCredential).
|
||||
if withinDir(workspace, stored) {
|
||||
t.Fatalf("the credential store %s is inside the workspace %s", stored, workspace)
|
||||
}
|
||||
if _, err := os.Stat(stored); !os.IsNotExist(err) {
|
||||
t.Fatalf("the credential store outlives the build: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(workspace, "git-credentials")); !os.IsNotExist(err) {
|
||||
t.Fatal("a credential file is left in the workspace")
|
||||
}
|
||||
}
|
||||
|
||||
// storeNamedIn is the credential store a git command line offers.
|
||||
func storeNamedIn(t *testing.T, line string) string {
|
||||
@@ -472,57 +181,6 @@ func storeNamedIn(t *testing.T, line string) string {
|
||||
|
||||
// Without a credential, a clone is exactly the invocation it always was, and no credential file
|
||||
// appears — the builder a mesh of public repositories runs is unchanged.
|
||||
func TestABuildWithNoCredentialClonesExactlyAsBefore(t *testing.T) {
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{
|
||||
"Dockerfile": "FROM scratch", "files/theme.conf": "dark",
|
||||
})
|
||||
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "",
|
||||
workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasPrefix(r.ran[0], "git clone --quiet ") {
|
||||
t.Fatalf("a credential-less clone grew flags: %s", r.ran[0])
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(workspace, "git-credentials")); !os.IsNotExist(err) {
|
||||
t.Fatal("a credential file was written with no credential to put in it")
|
||||
}
|
||||
}
|
||||
|
||||
// An artifact's own context is cloned with the same offer: a private module whose context is a
|
||||
// second private repository on the same forge builds, and the secret still never reaches argv.
|
||||
func TestAContextCloneCarriesTheSameCredentialStore(t *testing.T) {
|
||||
const withContext = `{"module":"route-proxy","version":"1",
|
||||
"build":{"artifacts":[
|
||||
{"name":"server","kind":"image","from":"Dockerfile",
|
||||
"context":{"repository":"https://forge.invalid/source.git","ref":"main"}}]}}`
|
||||
r := &recorded{
|
||||
contents: map[string]string{
|
||||
ManifestName: withContext,
|
||||
"Dockerfile": "FROM scratch\nCOPY go.mod ./\n",
|
||||
},
|
||||
secondary: map[string]map[string]string{
|
||||
"https://forge.invalid/source.git": {"go.mod": "module route-proxy\n"},
|
||||
},
|
||||
}
|
||||
workspace := t.TempDir()
|
||||
_, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/catalogue.git", "", "", workspace, nil, Npmrc{},
|
||||
GitCredential{URL: "https://builder:s3cret@forge.invalid"}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored := storeNamedIn(t, r.ran[0])
|
||||
var contextClone string
|
||||
for _, line := range r.ran {
|
||||
if strings.Contains(line, "clone") && strings.Contains(line, "source.git") {
|
||||
contextClone = line
|
||||
}
|
||||
}
|
||||
if contextClone == "" {
|
||||
t.Fatalf("the context was never cloned: %v", r.ran)
|
||||
}
|
||||
if !strings.Contains(contextClone, "credential.helper=store --file="+stored) {
|
||||
t.Fatalf("the context clone does not name the credential store: %s", contextClone)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestABundleIsCompiledAndPackedWithNoDockerfile(t *testing.T) {
|
||||
r, workspace := aRepository(t, aBundle, map[string]string{"index.ts": "console.log(1)"})
|
||||
held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}
|
||||
|
||||
got, err := Build(context.Background(), compiling{r}.run, r,
|
||||
"https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("a module with a language and no Dockerfile did not build: %v", err)
|
||||
}
|
||||
|
||||
// Compiled in the toolchain the mesh chose, not in one the module named.
|
||||
var compiled string
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker run") {
|
||||
compiled = line
|
||||
}
|
||||
}
|
||||
if compiled == "" {
|
||||
t.Fatalf("nothing was compiled:\n%s", strings.Join(r.ran, "\n"))
|
||||
}
|
||||
if !strings.Contains(compiled, "mesh-tools/build@sha256:") {
|
||||
t.Fatalf("the compile did not run in the mesh's own toolchain: %s", compiled)
|
||||
}
|
||||
if strings.Contains(strings.Join(r.ran, "\n"), "docker build") {
|
||||
t.Fatalf("a bundle invoked a Dockerfile build, which is the thing it exists to avoid:\n%s",
|
||||
strings.Join(r.ran, "\n"))
|
||||
}
|
||||
|
||||
// And pinned by a digest of what came out, like any other artifact.
|
||||
digest, _ := got.Manifest.Resources[0]["digest"].(string)
|
||||
if !strings.HasPrefix(digest, "sha256:") {
|
||||
t.Fatalf("the bundle was not pinned: %v", got.Manifest.Resources[0])
|
||||
}
|
||||
|
||||
// **One file per entrypoint and launcher, in the toolchain's bundler** (novox/hq ADR 0193). A
|
||||
// second run in the same toolchain image bundles each into the artifact's bundled output, the SDK
|
||||
// inlined, refusing by name in an image that predates the bundler; and the toolchain's
|
||||
// node_modules is no longer copied into a bundle that keeps nothing external.
|
||||
var bundling []string
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker run") && strings.Contains(line, "esbuild") {
|
||||
bundling = append(bundling, line)
|
||||
}
|
||||
}
|
||||
if len(bundling) != 2 {
|
||||
t.Fatalf("want one bundling run for the entrypoints and one for the launchers:\n%s", strings.Join(r.ran, "\n"))
|
||||
}
|
||||
for _, want := range []string{"mesh-tools/build@sha256:", "predates one-file bundles", "--bundle", "--format=esm",
|
||||
"--platform=node", "--outdir=" + Out("code") + ".bundled", Out("code") + "/index.js"} {
|
||||
if !strings.Contains(bundling[0], want) {
|
||||
t.Errorf("the entrypoints' bundling lacks %q: %s", want, bundling[0])
|
||||
}
|
||||
}
|
||||
if !strings.Contains(bundling[1], Out("code")+"/index.serve.mjs") || !strings.Contains(bundling[1], "--out-extension:.js=.mjs") {
|
||||
t.Errorf("the launcher is not bundled under its own name: %s", bundling[1])
|
||||
}
|
||||
if strings.Contains(strings.Join(r.ran, "\n"), "/app/runtime") {
|
||||
t.Errorf("the toolchain's node_modules was copied into a bundle that keeps nothing external:\n%s", strings.Join(r.ran, "\n"))
|
||||
}
|
||||
if strings.Index(strings.Join(r.ran, "\n"), "--outDir") > strings.Index(strings.Join(r.ran, "\n"), "esbuild") {
|
||||
t.Fatal("the bundler ran before the compile wrote its output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestABundleKeepingAPackageExternalCarriesTheToolchainsModules(t *testing.T) {
|
||||
manifest := strings.Replace(aBundle, `"entrypoints":["index.js"]`, `"entrypoints":["index.js"],"external":["sharp"]`, 1)
|
||||
r, workspace := aRepository(t, manifest, map[string]string{"index.ts": "console.log(1)"})
|
||||
held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}
|
||||
if _, err := Build(context.Background(), compiling{r}.run, r,
|
||||
"https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, GitCredential{}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
all := strings.Join(r.ran, "\n")
|
||||
if !strings.Contains(all, "--external:sharp") || !strings.Contains(all, "/app/runtime") {
|
||||
t.Errorf("an external package was not kept as an import with the toolchain's modules beside it:\n%s", all)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABundleWhoseToolchainIsNotHeldIsRefusedFirst(t *testing.T) {
|
||||
r, workspace := aRepository(t, aBundle, map[string]string{"index.ts": "console.log(1)"})
|
||||
|
||||
_, err := Build(context.Background(), compiling{r}.run, r,
|
||||
"https://forge.invalid/greeter.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a bundle was built with no toolchain to compile it in")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "mesh-tools") {
|
||||
t.Fatalf("the refusal does not name what has to be built first: %v", err)
|
||||
}
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker run") {
|
||||
t.Fatalf("a compile was attempted before the refusal: %s", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestABundleInAnUnknownLanguageIsRefused(t *testing.T) {
|
||||
manifest := strings.Replace(aBundle, `"language":"typescript"`, `"language":"cobol"`, 1)
|
||||
r, workspace := aRepository(t, manifest, map[string]string{"index.ts": "x"})
|
||||
|
||||
_, err := Build(context.Background(), compiling{r}.run, r,
|
||||
"https://forge.invalid/greeter.git", "", "", workspace,
|
||||
map[string]string{"mesh-tools/build": "registry.invalid/x@sha256:" + strings.Repeat("c", 64)}, Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a language nothing can compile was accepted")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "typescript") {
|
||||
t.Fatalf("the refusal does not say what would have worked: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTwoBundlesInOneModuleArePackedSeparately(t *testing.T) {
|
||||
const two = `{"module":"greeter","version":"1",
|
||||
"build":{"artifacts":[
|
||||
{"name":"daemon","kind":"bundle","language":"typescript","entrypoints":["index.js"]},
|
||||
{"name":"tools","kind":"bundle","language":"typescript","entrypoints":["index.js"]}]},
|
||||
"resources":[
|
||||
{"id":"a","type":"archive","path":"/opt/greeter/daemon","artifact":"daemon"},
|
||||
{"id":"b","type":"archive","path":"/opt/greeter/tools","artifact":"tools"}]}`
|
||||
|
||||
r, workspace := aRepository(t, two, map[string]string{"index.ts": "console.log(1)"})
|
||||
held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}
|
||||
|
||||
got, err := Build(context.Background(), compiling{r}.run, r,
|
||||
"https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("a module with two bundles did not build: %v", err)
|
||||
}
|
||||
|
||||
// Compiled into two different places. Only the compile lines: the copy of each bundle's
|
||||
// dependencies names the same directory again, deliberately.
|
||||
var outputs []string
|
||||
for _, line := range r.ran {
|
||||
if !strings.Contains(line, "--outDir") {
|
||||
continue
|
||||
}
|
||||
for _, part := range strings.Fields(line) {
|
||||
if strings.HasPrefix(part, ".mesh-build/") {
|
||||
outputs = append(outputs, part)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(outputs) != 2 || outputs[0] == outputs[1] {
|
||||
t.Fatalf("two bundles did not get their own output directories: %v", outputs)
|
||||
}
|
||||
|
||||
// And published as two artifacts, each with its own digest.
|
||||
if len(r.archives) != 2 {
|
||||
t.Fatalf("expected two archives published, got %v", r.archives)
|
||||
}
|
||||
first, _ := got.Manifest.Resources[0]["digest"].(string)
|
||||
second, _ := got.Manifest.Resources[1]["digest"].(string)
|
||||
if first == "" || second == "" {
|
||||
t.Fatalf("a bundle was not pinned: %v", got.Manifest.Resources)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -49,85 +48,9 @@ func (c compiling) run(ctx context.Context, dir, name string, args ...string) (s
|
||||
// **A module says what it is written in, and needs no Dockerfile.** This is the whole point of the
|
||||
// bundle recipe: the same module previously needed a hand-written recipe repeating an incantation
|
||||
// that is easy to get wrong in ways that fail somewhere else.
|
||||
func TestABundleIsCompiledAndPackedWithNoDockerfile(t *testing.T) {
|
||||
r, workspace := aRepository(t, aBundle, map[string]string{"index.ts": "console.log(1)"})
|
||||
held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}
|
||||
|
||||
got, err := Build(context.Background(), compiling{r}.run, r,
|
||||
"https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("a module with a language and no Dockerfile did not build: %v", err)
|
||||
}
|
||||
|
||||
// Compiled in the toolchain the mesh chose, not in one the module named.
|
||||
var compiled string
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker run") {
|
||||
compiled = line
|
||||
}
|
||||
}
|
||||
if compiled == "" {
|
||||
t.Fatalf("nothing was compiled:\n%s", strings.Join(r.ran, "\n"))
|
||||
}
|
||||
if !strings.Contains(compiled, "mesh-tools/build@sha256:") {
|
||||
t.Fatalf("the compile did not run in the mesh's own toolchain: %s", compiled)
|
||||
}
|
||||
if strings.Contains(strings.Join(r.ran, "\n"), "docker build") {
|
||||
t.Fatalf("a bundle invoked a Dockerfile build, which is the thing it exists to avoid:\n%s",
|
||||
strings.Join(r.ran, "\n"))
|
||||
}
|
||||
|
||||
// And pinned by a digest of what came out, like any other artifact.
|
||||
digest, _ := got.Manifest.Resources[0]["digest"].(string)
|
||||
if !strings.HasPrefix(digest, "sha256:") {
|
||||
t.Fatalf("the bundle was not pinned: %v", got.Manifest.Resources[0])
|
||||
}
|
||||
|
||||
// **One file per entrypoint and launcher, in the toolchain's bundler** (novox/hq ADR 0193). A
|
||||
// second run in the same toolchain image bundles each into the artifact's bundled output, the SDK
|
||||
// inlined, refusing by name in an image that predates the bundler; and the toolchain's
|
||||
// node_modules is no longer copied into a bundle that keeps nothing external.
|
||||
var bundling []string
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker run") && strings.Contains(line, "esbuild") {
|
||||
bundling = append(bundling, line)
|
||||
}
|
||||
}
|
||||
if len(bundling) != 2 {
|
||||
t.Fatalf("want one bundling run for the entrypoints and one for the launchers:\n%s", strings.Join(r.ran, "\n"))
|
||||
}
|
||||
for _, want := range []string{"mesh-tools/build@sha256:", "predates one-file bundles", "--bundle", "--format=esm",
|
||||
"--platform=node", "--outdir=" + Out("code") + ".bundled", Out("code") + "/index.js"} {
|
||||
if !strings.Contains(bundling[0], want) {
|
||||
t.Errorf("the entrypoints' bundling lacks %q: %s", want, bundling[0])
|
||||
}
|
||||
}
|
||||
if !strings.Contains(bundling[1], Out("code")+"/index.serve.mjs") || !strings.Contains(bundling[1], "--out-extension:.js=.mjs") {
|
||||
t.Errorf("the launcher is not bundled under its own name: %s", bundling[1])
|
||||
}
|
||||
if strings.Contains(strings.Join(r.ran, "\n"), "/app/runtime") {
|
||||
t.Errorf("the toolchain's node_modules was copied into a bundle that keeps nothing external:\n%s", strings.Join(r.ran, "\n"))
|
||||
}
|
||||
if strings.Index(strings.Join(r.ran, "\n"), "--outDir") > strings.Index(strings.Join(r.ran, "\n"), "esbuild") {
|
||||
t.Fatal("the bundler ran before the compile wrote its output")
|
||||
}
|
||||
}
|
||||
|
||||
// A bundle naming packages it keeps external is bundled with them as imports, and carries the
|
||||
// toolchain's node_modules for them — the one case it still does.
|
||||
func TestABundleKeepingAPackageExternalCarriesTheToolchainsModules(t *testing.T) {
|
||||
manifest := strings.Replace(aBundle, `"entrypoints":["index.js"]`, `"entrypoints":["index.js"],"external":["sharp"]`, 1)
|
||||
r, workspace := aRepository(t, manifest, map[string]string{"index.ts": "console.log(1)"})
|
||||
held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}
|
||||
if _, err := Build(context.Background(), compiling{r}.run, r,
|
||||
"https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, GitCredential{}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
all := strings.Join(r.ran, "\n")
|
||||
if !strings.Contains(all, "--external:sharp") || !strings.Contains(all, "/app/runtime") {
|
||||
t.Errorf("an external package was not kept as an import with the toolchain's modules beside it:\n%s", all)
|
||||
}
|
||||
}
|
||||
|
||||
// A language whose bundle carries its own dependencies copies nothing in: a Go binary is static.
|
||||
func TestOnlyALanguageWithARuntimeDirectoryCopiesDependenciesIn(t *testing.T) {
|
||||
@@ -146,39 +69,8 @@ func TestOnlyALanguageWithARuntimeDirectoryCopiesDependenciesIn(t *testing.T) {
|
||||
// **Refused before anything is built, naming what to build first.** A base the mesh has not built
|
||||
// is not a compile that fails on its first line — it is a question somebody can answer, and saying
|
||||
// it early is the difference between a fixable message and one about a missing image.
|
||||
func TestABundleWhoseToolchainIsNotHeldIsRefusedFirst(t *testing.T) {
|
||||
r, workspace := aRepository(t, aBundle, map[string]string{"index.ts": "console.log(1)"})
|
||||
|
||||
_, err := Build(context.Background(), compiling{r}.run, r,
|
||||
"https://forge.invalid/greeter.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a bundle was built with no toolchain to compile it in")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "mesh-tools") {
|
||||
t.Fatalf("the refusal does not name what has to be built first: %v", err)
|
||||
}
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker run") {
|
||||
t.Fatalf("a compile was attempted before the refusal: %s", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A language the mesh does not build is refused the same way, and names what it can build.
|
||||
func TestABundleInAnUnknownLanguageIsRefused(t *testing.T) {
|
||||
manifest := strings.Replace(aBundle, `"language":"typescript"`, `"language":"cobol"`, 1)
|
||||
r, workspace := aRepository(t, manifest, map[string]string{"index.ts": "x"})
|
||||
|
||||
_, err := Build(context.Background(), compiling{r}.run, r,
|
||||
"https://forge.invalid/greeter.git", "", "", workspace,
|
||||
map[string]string{"mesh-tools/build": "registry.invalid/x@sha256:" + strings.Repeat("c", 64)}, Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a language nothing can compile was accepted")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "typescript") {
|
||||
t.Fatalf("the refusal does not say what would have worked: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// **One module, two bundles, and neither packs the other.**
|
||||
//
|
||||
@@ -186,48 +78,3 @@ func TestABundleInAnUnknownLanguageIsRefused(t *testing.T) {
|
||||
// daemon in one language and tools in another (ADR 0040). An earlier version of this compiled
|
||||
// every bundle into the toolchain's single output directory, so two of them would overwrite each
|
||||
// other and then be packed together — one artifact containing both, twice.
|
||||
func TestTwoBundlesInOneModuleArePackedSeparately(t *testing.T) {
|
||||
const two = `{"module":"greeter","version":"1",
|
||||
"build":{"artifacts":[
|
||||
{"name":"daemon","kind":"bundle","language":"typescript","entrypoints":["index.js"]},
|
||||
{"name":"tools","kind":"bundle","language":"typescript","entrypoints":["index.js"]}]},
|
||||
"resources":[
|
||||
{"id":"a","type":"archive","path":"/opt/greeter/daemon","artifact":"daemon"},
|
||||
{"id":"b","type":"archive","path":"/opt/greeter/tools","artifact":"tools"}]}`
|
||||
|
||||
r, workspace := aRepository(t, two, map[string]string{"index.ts": "console.log(1)"})
|
||||
held := map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}
|
||||
|
||||
got, err := Build(context.Background(), compiling{r}.run, r,
|
||||
"https://forge.invalid/greeter.git", "", "", workspace, held, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("a module with two bundles did not build: %v", err)
|
||||
}
|
||||
|
||||
// Compiled into two different places. Only the compile lines: the copy of each bundle's
|
||||
// dependencies names the same directory again, deliberately.
|
||||
var outputs []string
|
||||
for _, line := range r.ran {
|
||||
if !strings.Contains(line, "--outDir") {
|
||||
continue
|
||||
}
|
||||
for _, part := range strings.Fields(line) {
|
||||
if strings.HasPrefix(part, ".mesh-build/") {
|
||||
outputs = append(outputs, part)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(outputs) != 2 || outputs[0] == outputs[1] {
|
||||
t.Fatalf("two bundles did not get their own output directories: %v", outputs)
|
||||
}
|
||||
|
||||
// And published as two artifacts, each with its own digest.
|
||||
if len(r.archives) != 2 {
|
||||
t.Fatalf("expected two archives published, got %v", r.archives)
|
||||
}
|
||||
first, _ := got.Manifest.Resources[0]["digest"].(string)
|
||||
second, _ := got.Manifest.Resources[1]["digest"].(string)
|
||||
if first == "" || second == "" {
|
||||
t.Fatalf("a bundle was not pinned: %v", got.Manifest.Resources)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestACheckContainerSeesNoForgeCredential(t *testing.T) {
|
||||
repo, head := aCheckedRepository(t, map[string]string{CheckScript: "echo checked\n"})
|
||||
besideRepo, besideHead := aCheckedRepository(t, map[string]string{"README": "beside"})
|
||||
// A clone source that carries userinfo, as a forge's clone URL may: git records it as given in the
|
||||
// clone's .git/config, which the container reads.
|
||||
besideURL := "file://beside-user:" + besideSecret + "@" + besideRepo
|
||||
workspace := t.TempDir()
|
||||
|
||||
var stores []string
|
||||
reached := false
|
||||
var leaks []string
|
||||
run := func(ctx context.Context, dir, name string, args ...string) (string, error) {
|
||||
switch name {
|
||||
case "git":
|
||||
for _, a := range args {
|
||||
if f, ok := strings.CutPrefix(a, "credential.helper=store --file="); ok {
|
||||
stores = append(stores, f)
|
||||
raw, err := os.ReadFile(f)
|
||||
if err != nil || strings.TrimSpace(string(raw)) != forgeURL {
|
||||
t.Errorf("git is offered a store that does not hold the credential as given: %q, %v", raw, err)
|
||||
}
|
||||
if info, err := os.Stat(f); err == nil && info.Mode().Perm() != 0o600 {
|
||||
t.Errorf("the credential store is readable beyond its owner: %v", info.Mode())
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(args) >= 2 && args[len(args)-3] == "--quiet" && hasString(args, "clone") {
|
||||
source := args[len(args)-2]
|
||||
if u, err := url.Parse(source); err == nil && u.User != nil {
|
||||
// Git cannot reach a file:// URL with userinfo; clone it without, then record it as git
|
||||
// would have: as given.
|
||||
clone := append(append([]string{}, args[:len(args)-2]...), bareURL(t, source), args[len(args)-1])
|
||||
if out, err := Command(ctx, dir, "git", clone...); err != nil {
|
||||
return out, err
|
||||
}
|
||||
return Command(ctx, filepath.Join(dir, args[len(args)-1]), "git", "remote", "set-url", "origin", source)
|
||||
}
|
||||
}
|
||||
return Command(ctx, dir, name, args...)
|
||||
case "docker":
|
||||
if !reached {
|
||||
reached = true
|
||||
// The first container: everything the workspace holds is what the toolchain container sees.
|
||||
filepath.WalkDir(workspace, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
raw, _ := os.ReadFile(path)
|
||||
s := string(raw)
|
||||
if strings.Contains(s, forgeSecret) || strings.Contains(s, besideSecret) ||
|
||||
strings.Contains(s, npmSecret) || d.Name() == "git-credentials" || d.Name() == ".npmrc" ||
|
||||
strings.Contains(s, "credential.helper") {
|
||||
leaks = append(leaks, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
for _, f := range stores {
|
||||
if _, err := os.Stat(f); !errors.Is(err, os.ErrNotExist) {
|
||||
leaks = append(leaks, f+" (still there when the first container runs)")
|
||||
}
|
||||
if rel, err := filepath.Rel(workspace, f); err == nil && !strings.HasPrefix(rel, "..") {
|
||||
leaks = append(leaks, f+" (inside the workspace the container mounts)")
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(args) > 0 && args[0] == "ps" {
|
||||
return "", nil
|
||||
}
|
||||
return "", errors.New("no container runtime in this test")
|
||||
}
|
||||
return "", errors.New("unexpected command " + name)
|
||||
}
|
||||
// A credential an older builder left in the workspace is removed too: the forge's, and the .npmrc a
|
||||
// build wrote into the tree it cloned.
|
||||
if err := os.WriteFile(filepath.Join(workspace, "git-credentials"), []byte(forgeURL+"\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, dir := range []string{"source/x", "context-server"} {
|
||||
if err := os.MkdirAll(filepath.Join(workspace, dir), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(workspace, dir, ".npmrc"),
|
||||
[]byte("//forge.invalid/api/packages/novox/npm/:_authToken="+npmSecret+"\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
_, err := Check(t.Context(), run, CheckSpec{ID: "check-462", Repository: repo, Ref: head, Owner: "novox",
|
||||
Repo: "mesh-controller", Number: 1, Toolchain: "golang", Beside: map[string]Beside{
|
||||
"mesh-catalog": {Repository: besideURL, Ref: besideHead}}}, workspace, aFactsRegistry(t),
|
||||
GitCredential{URL: forgeURL}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("the check ran past its first container in a test with none")
|
||||
}
|
||||
if !reached {
|
||||
t.Fatalf("the check never reached its first container: %v", err)
|
||||
}
|
||||
if len(stores) == 0 {
|
||||
t.Fatal("no clone was offered the forge credential")
|
||||
}
|
||||
if len(leaks) > 0 {
|
||||
t.Fatalf("the check's container sees the credential:\n%s", strings.Join(leaks, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestACheckLinePublishedToTheLogIsRedacted(t *testing.T) {
|
||||
var said []string
|
||||
say := func(step, format string, args ...any) {
|
||||
if step == "output" && len(args) > 0 {
|
||||
said = append(said, args[0].(string))
|
||||
}
|
||||
}
|
||||
var out tail
|
||||
layer := ownCheck(t.Context(), CheckSpec{Toolchain: "golang"}, []ScriptPart{{Toolchain: "go", Script: CheckScript}},
|
||||
t.TempDir(), &out, func() bool { return false }, func(string, string) *exec.Cmd {
|
||||
return exec.CommandContext(t.Context(), "sh", "-c", "echo cloning http://mesh_builder:t0ps3cret-forge@forge.invalid/novox/x.git; "+
|
||||
"echo token ghp_abcdefghijklmnopqrstuvwxyz0123456789")
|
||||
}, say)
|
||||
if layer == nil || layer.Verdict != "pass" {
|
||||
t.Fatalf("the check answered %+v\n%s", layer, out.String())
|
||||
}
|
||||
joined := strings.Join(said, "\n")
|
||||
if strings.Contains(joined, "t0ps3cret-forge") || strings.Contains(joined, "ghp_abcdef") {
|
||||
t.Fatalf("a secret the check printed is published to the build's log:\n%s", joined)
|
||||
}
|
||||
if !strings.Contains(joined, "http://mesh_builder:[redacted: a password in a URI]@forge.invalid/novox/x.git") {
|
||||
t.Fatalf("the line is not said with what was there named:\n%s", joined)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABuildsCloneRecordsNoUserinfo(t *testing.T) {
|
||||
repo, _ := aCheckedRepository(t, map[string]string{ManifestName: `{"module":"plain","version":"1"}`})
|
||||
source := "file://build-user:" + besideSecret + "@" + repo
|
||||
workspace := t.TempDir()
|
||||
tree := filepath.Join(workspace, "source")
|
||||
var configs []string
|
||||
run := func(ctx context.Context, dir, name string, args ...string) (string, error) {
|
||||
if name == "git" && hasString(args, "clone") && args[len(args)-2] == source {
|
||||
clone := append(append([]string{}, args[:len(args)-2]...), bareURL(t, source), args[len(args)-1])
|
||||
if out, err := Command(ctx, dir, "git", clone...); err != nil {
|
||||
return out, err
|
||||
}
|
||||
return Command(ctx, args[len(args)-1], "git", "remote", "set-url", "origin", source)
|
||||
}
|
||||
if name == "git" && len(args) > 0 && args[0] == "rev-parse" {
|
||||
raw, _ := os.ReadFile(filepath.Join(tree, ".git", "config"))
|
||||
configs = append(configs, string(raw))
|
||||
}
|
||||
return Command(ctx, dir, name, args...)
|
||||
}
|
||||
if _, err := Build(t.Context(), run, &recorded{}, source, "", "", workspace, nil, Npmrc{}, GitCredential{}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(configs) == 0 {
|
||||
t.Fatal("the build never read its clone")
|
||||
}
|
||||
for _, c := range configs {
|
||||
if strings.Contains(c, besideSecret) || strings.Contains(c, "build-user") {
|
||||
t.Fatalf("the build's clone records the credential it was cloned with:\n%s", c)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(tree); !os.IsNotExist(err) {
|
||||
t.Fatalf("the build's tree outlives the build: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,12 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -67,135 +61,7 @@ func bareURL(t *testing.T, raw string) string {
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func TestACheckContainerSeesNoForgeCredential(t *testing.T) {
|
||||
repo, head := aCheckedRepository(t, map[string]string{CheckScript: "echo checked\n"})
|
||||
besideRepo, besideHead := aCheckedRepository(t, map[string]string{"README": "beside"})
|
||||
// A clone source that carries userinfo, as a forge's clone URL may: git records it as given in the
|
||||
// clone's .git/config, which the container reads.
|
||||
besideURL := "file://beside-user:" + besideSecret + "@" + besideRepo
|
||||
workspace := t.TempDir()
|
||||
|
||||
var stores []string
|
||||
reached := false
|
||||
var leaks []string
|
||||
run := func(ctx context.Context, dir, name string, args ...string) (string, error) {
|
||||
switch name {
|
||||
case "git":
|
||||
for _, a := range args {
|
||||
if f, ok := strings.CutPrefix(a, "credential.helper=store --file="); ok {
|
||||
stores = append(stores, f)
|
||||
raw, err := os.ReadFile(f)
|
||||
if err != nil || strings.TrimSpace(string(raw)) != forgeURL {
|
||||
t.Errorf("git is offered a store that does not hold the credential as given: %q, %v", raw, err)
|
||||
}
|
||||
if info, err := os.Stat(f); err == nil && info.Mode().Perm() != 0o600 {
|
||||
t.Errorf("the credential store is readable beyond its owner: %v", info.Mode())
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(args) >= 2 && args[len(args)-3] == "--quiet" && hasString(args, "clone") {
|
||||
source := args[len(args)-2]
|
||||
if u, err := url.Parse(source); err == nil && u.User != nil {
|
||||
// Git cannot reach a file:// URL with userinfo; clone it without, then record it as git
|
||||
// would have: as given.
|
||||
clone := append(append([]string{}, args[:len(args)-2]...), bareURL(t, source), args[len(args)-1])
|
||||
if out, err := Command(ctx, dir, "git", clone...); err != nil {
|
||||
return out, err
|
||||
}
|
||||
return Command(ctx, filepath.Join(dir, args[len(args)-1]), "git", "remote", "set-url", "origin", source)
|
||||
}
|
||||
}
|
||||
return Command(ctx, dir, name, args...)
|
||||
case "docker":
|
||||
if !reached {
|
||||
reached = true
|
||||
// The first container: everything the workspace holds is what the toolchain container sees.
|
||||
filepath.WalkDir(workspace, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
raw, _ := os.ReadFile(path)
|
||||
s := string(raw)
|
||||
if strings.Contains(s, forgeSecret) || strings.Contains(s, besideSecret) ||
|
||||
strings.Contains(s, npmSecret) || d.Name() == "git-credentials" || d.Name() == ".npmrc" ||
|
||||
strings.Contains(s, "credential.helper") {
|
||||
leaks = append(leaks, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
for _, f := range stores {
|
||||
if _, err := os.Stat(f); !errors.Is(err, os.ErrNotExist) {
|
||||
leaks = append(leaks, f+" (still there when the first container runs)")
|
||||
}
|
||||
if rel, err := filepath.Rel(workspace, f); err == nil && !strings.HasPrefix(rel, "..") {
|
||||
leaks = append(leaks, f+" (inside the workspace the container mounts)")
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(args) > 0 && args[0] == "ps" {
|
||||
return "", nil
|
||||
}
|
||||
return "", errors.New("no container runtime in this test")
|
||||
}
|
||||
return "", errors.New("unexpected command " + name)
|
||||
}
|
||||
// A credential an older builder left in the workspace is removed too: the forge's, and the .npmrc a
|
||||
// build wrote into the tree it cloned.
|
||||
if err := os.WriteFile(filepath.Join(workspace, "git-credentials"), []byte(forgeURL+"\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, dir := range []string{"source/x", "context-server"} {
|
||||
if err := os.MkdirAll(filepath.Join(workspace, dir), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(workspace, dir, ".npmrc"),
|
||||
[]byte("//forge.invalid/api/packages/novox/npm/:_authToken="+npmSecret+"\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
_, err := Check(t.Context(), run, CheckSpec{ID: "check-462", Repository: repo, Ref: head, Owner: "novox",
|
||||
Repo: "mesh-controller", Number: 1, Toolchain: "golang", Beside: map[string]Beside{
|
||||
"mesh-catalog": {Repository: besideURL, Ref: besideHead}}}, workspace, aFactsRegistry(t),
|
||||
GitCredential{URL: forgeURL}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("the check ran past its first container in a test with none")
|
||||
}
|
||||
if !reached {
|
||||
t.Fatalf("the check never reached its first container: %v", err)
|
||||
}
|
||||
if len(stores) == 0 {
|
||||
t.Fatal("no clone was offered the forge credential")
|
||||
}
|
||||
if len(leaks) > 0 {
|
||||
t.Fatalf("the check's container sees the credential:\n%s", strings.Join(leaks, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
// Every line a repository's own check prints is published to the build's log redacted.
|
||||
func TestACheckLinePublishedToTheLogIsRedacted(t *testing.T) {
|
||||
var said []string
|
||||
say := func(step, format string, args ...any) {
|
||||
if step == "output" && len(args) > 0 {
|
||||
said = append(said, args[0].(string))
|
||||
}
|
||||
}
|
||||
var out tail
|
||||
layer := ownCheck(t.Context(), CheckSpec{Toolchain: "golang"}, []ScriptPart{{Toolchain: "go", Script: CheckScript}},
|
||||
t.TempDir(), &out, func() bool { return false }, func(string, string) *exec.Cmd {
|
||||
return exec.CommandContext(t.Context(), "sh", "-c", "echo cloning http://mesh_builder:t0ps3cret-forge@forge.invalid/novox/x.git; "+
|
||||
"echo token ghp_abcdefghijklmnopqrstuvwxyz0123456789")
|
||||
}, say)
|
||||
if layer == nil || layer.Verdict != "pass" {
|
||||
t.Fatalf("the check answered %+v\n%s", layer, out.String())
|
||||
}
|
||||
joined := strings.Join(said, "\n")
|
||||
if strings.Contains(joined, "t0ps3cret-forge") || strings.Contains(joined, "ghp_abcdef") {
|
||||
t.Fatalf("a secret the check printed is published to the build's log:\n%s", joined)
|
||||
}
|
||||
if !strings.Contains(joined, "http://mesh_builder:[redacted: a password in a URI]@forge.invalid/novox/x.git") {
|
||||
t.Fatalf("the line is not said with what was there named:\n%s", joined)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheRedactorHidesTheForgeCredentialAndShapes(t *testing.T) {
|
||||
r := redactorFor(GitCredential{URL: forgeURL})
|
||||
@@ -214,38 +80,3 @@ func TestTheRedactorHidesTheForgeCredentialAndShapes(t *testing.T) {
|
||||
// A build's clone of a URL carrying userinfo records it without, so no build leaves a credential in
|
||||
// workspace/source/.git/config while it runs either (novox/hq issue 462); the tree itself is gone when the
|
||||
// build ends.
|
||||
func TestABuildsCloneRecordsNoUserinfo(t *testing.T) {
|
||||
repo, _ := aCheckedRepository(t, map[string]string{ManifestName: `{"module":"plain","version":"1"}`})
|
||||
source := "file://build-user:" + besideSecret + "@" + repo
|
||||
workspace := t.TempDir()
|
||||
tree := filepath.Join(workspace, "source")
|
||||
var configs []string
|
||||
run := func(ctx context.Context, dir, name string, args ...string) (string, error) {
|
||||
if name == "git" && hasString(args, "clone") && args[len(args)-2] == source {
|
||||
clone := append(append([]string{}, args[:len(args)-2]...), bareURL(t, source), args[len(args)-1])
|
||||
if out, err := Command(ctx, dir, "git", clone...); err != nil {
|
||||
return out, err
|
||||
}
|
||||
return Command(ctx, args[len(args)-1], "git", "remote", "set-url", "origin", source)
|
||||
}
|
||||
if name == "git" && len(args) > 0 && args[0] == "rev-parse" {
|
||||
raw, _ := os.ReadFile(filepath.Join(tree, ".git", "config"))
|
||||
configs = append(configs, string(raw))
|
||||
}
|
||||
return Command(ctx, dir, name, args...)
|
||||
}
|
||||
if _, err := Build(t.Context(), run, &recorded{}, source, "", "", workspace, nil, Npmrc{}, GitCredential{}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(configs) == 0 {
|
||||
t.Fatal("the build never read its clone")
|
||||
}
|
||||
for _, c := range configs {
|
||||
if strings.Contains(c, besideSecret) || strings.Contains(c, "build-user") {
|
||||
t.Fatalf("the build's clone records the credential it was cloned with:\n%s", c)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(tree); !os.IsNotExist(err) {
|
||||
t.Fatalf("the build's tree outlives the build: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,399 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTheThrowawayStoreFlushesNothing(t *testing.T) {
|
||||
said := strings.Join(StoreSettings, " ")
|
||||
for _, want := range []string{"-c fsync=off", "-c synchronous_commit=off", "-c full_page_writes=off"} {
|
||||
if !strings.Contains(said, want) {
|
||||
t.Errorf("the store is raised with %q, not %q", said, want)
|
||||
}
|
||||
}
|
||||
if os.Getenv("MESH_TEST_DOCKER") != "1" {
|
||||
t.Skip("MESH_TEST_DOCKER=1: the settings are proven on a raised store")
|
||||
}
|
||||
id := fmt.Sprintf("check-store-%d", time.Now().UnixNano())
|
||||
labelledRun := Labelled(Command, id)
|
||||
t.Cleanup(func() { _, _ = RemoveContainersOf(context.Background(), Command, id) })
|
||||
if _, err := throwaway(t.Context(), labelledRun, Command, id+"-store", StoreImage("17.11"), 5432,
|
||||
[]string{"-e", "POSTGRES_PASSWORD=check"}, StoreSettings); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := waitFor(t.Context(), Command, id+"-store", []string{"pg_isready", "-U", "postgres"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for setting, want := range map[string]string{"fsync": "off", "synchronous_commit": "off", "full_page_writes": "off"} {
|
||||
var got string
|
||||
for try := 0; try < 10; try++ {
|
||||
out, err := exec.Command("docker", "exec", id+"-store", "psql", "-U", "postgres", "-tAc", "SHOW "+setting).CombinedOutput()
|
||||
if got = strings.TrimSpace(string(out)); err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("%s is %q on the raised store", setting, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func aCheckedRepository(t *testing.T, files map[string]string) (string, string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
git := func(args ...string) string {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.org",
|
||||
"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.org")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git %v: %v\n%s", args, err, out)
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
git("init", "--quiet", "-b", "main")
|
||||
for name, body := range files {
|
||||
if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, name)), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
git("add", "-A")
|
||||
git("commit", "--quiet", "-m", "x")
|
||||
return dir, git("rev-parse", "HEAD")
|
||||
}
|
||||
|
||||
func labelled(id string) []string {
|
||||
out, _ := exec.Command("docker", "ps", "-aq", "--filter", "label="+BuildLabel+"="+id).Output()
|
||||
return strings.Fields(string(out))
|
||||
}
|
||||
|
||||
func TestACheckRunsTheRepositorysOwnScriptBesideTheMeshsVersionsAndLeavesNothing(t *testing.T) {
|
||||
registry := checkEnvironment(t)
|
||||
// The script proves what it was given: the facts, a store that answers, a bus that answers — and no
|
||||
// container runtime socket.
|
||||
script := `set -e
|
||||
test -s "$MESH_FACTS"
|
||||
grep -q '"bus": "2.11.17"' "$MESH_FACTS" || grep -q '"bus":"2.11.17"' "$MESH_FACTS"
|
||||
case "$MESH_TEST_POSTGRES" in postgres://*127.0.0.1:*) ;; *) echo "no store: $MESH_TEST_POSTGRES"; exit 1;; esac
|
||||
case "$MESH_TEST_NATS" in nats://127.0.0.1:*) ;; *) echo "no bus: $MESH_TEST_NATS"; exit 1;; esac
|
||||
test "$MESH_CHECK_REPOSITORY" = "novox/hq"
|
||||
test "$MESH_CHECK_CHANGED" = "a.go,b.go"
|
||||
test ! -S /var/run/docker.sock || { echo "the check holds the container runtime's socket"; exit 1; }
|
||||
echo checked
|
||||
`
|
||||
repo, head := aCheckedRepository(t, map[string]string{CheckScript: script})
|
||||
id := fmt.Sprintf("check-test-%d", time.Now().UnixNano())
|
||||
v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: repo, Ref: head, Owner: "novox",
|
||||
Repo: "hq", Number: 7, Paths: []string{"a.go", "b.go"}, Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Nothing of the graph touched: the gate a pass that says so, the repository's own check run.
|
||||
if v.Gate == nil || v.Gate.Verdict != "pass" || v.Gate.Summary != noModule {
|
||||
t.Errorf("the gate answered %+v", v.Gate)
|
||||
}
|
||||
if v.Repo == nil || v.Repo.Verdict != "pass" || !strings.Contains(v.Report, "checked") {
|
||||
t.Fatalf("the repository's check answered %+v\n%s", v.Repo, v.Report)
|
||||
}
|
||||
if left := labelled(id); len(left) > 0 {
|
||||
t.Errorf("the check left %d container(s) behind", len(left))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAFailingCheckFailsAndOneThatCannotRunIsNeverAPass(t *testing.T) {
|
||||
registry := checkEnvironment(t)
|
||||
repo, head := aCheckedRepository(t, map[string]string{CheckScript: "echo 'resource \"x.service\": refused'; exit 3\n"})
|
||||
v, err := Check(t.Context(), Command, CheckSpec{ID: fmt.Sprintf("check-fail-%d", time.Now().UnixNano()),
|
||||
Repository: repo, Ref: head, Owner: "novox", Repo: "hq", Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Repo == nil || v.Repo.Verdict != "fail" || !strings.Contains(v.Repo.Summary, "refused") {
|
||||
t.Fatalf("a failing script answered %+v", v.Repo)
|
||||
}
|
||||
|
||||
// A script in a toolchain the mesh does not hold: an error, never a pass.
|
||||
repo, head = aCheckedRepository(t, map[string]string{CheckScript: "# mesh-check-toolchain: cobol\nexit 0\n"})
|
||||
v, err = Check(t.Context(), Command, CheckSpec{ID: fmt.Sprintf("check-cobol-%d", time.Now().UnixNano()),
|
||||
Repository: repo, Ref: head, Owner: "novox", Repo: "hq", Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err != nil || v.Repo == nil || v.Repo.Verdict != "error" {
|
||||
t.Fatalf("a script in a toolchain nobody holds answered %+v, %v", v.Repo, err)
|
||||
}
|
||||
|
||||
// Past its bound: an error, not a pass.
|
||||
was := CheckTimeout
|
||||
CheckTimeout = 25 * time.Second
|
||||
t.Cleanup(func() { CheckTimeout = was })
|
||||
repo, head = aCheckedRepository(t, map[string]string{CheckScript: "sleep 120\n"})
|
||||
v, err = Check(context.Background(), Command, CheckSpec{ID: fmt.Sprintf("check-slow-%d", time.Now().UnixNano()),
|
||||
Repository: repo, Ref: head, Owner: "novox", Repo: "hq", Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err == nil && (v.Repo == nil || v.Repo.Verdict != "error") {
|
||||
t.Fatalf("a check past its bound answered %+v", v.Repo)
|
||||
}
|
||||
|
||||
// No facts: it cannot run, and says so.
|
||||
repo, head = aCheckedRepository(t, map[string]string{CheckScript: "exit 0\n"})
|
||||
_, err = Check(t.Context(), Command, CheckSpec{ID: "check-nofacts", Repository: repo, Ref: head, Owner: "novox",
|
||||
Repo: "hq", Toolchain: goToolchain}, t.TempDir(), "127.0.0.1:1", GitCredential{}, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "facts snapshot") {
|
||||
t.Fatalf("a check with no facts said %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACheckWithNothingToRunSaysSo(t *testing.T) {
|
||||
repo, head := aCheckedRepository(t, map[string]string{"README.md": "x"})
|
||||
v, err := Check(t.Context(), Command, CheckSpec{ID: "check-nothing", Repository: repo, Ref: head, Owner: "novox",
|
||||
Repo: "hq"}, t.TempDir(), "", GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Gate == nil || v.Gate.Verdict != "pass" || v.Repo == nil || v.Repo.Verdict != "warning" ||
|
||||
!strings.Contains(v.Repo.Summary, CheckScript) {
|
||||
t.Fatalf("a check with nothing to run said %+v / %+v", v.Gate, v.Repo)
|
||||
}
|
||||
// A gated change never takes that path: with no store to read the facts from, it cannot run.
|
||||
_, err = Check(t.Context(), Command, CheckSpec{ID: "check-gated", Repository: repo, Ref: head, Owner: "novox",
|
||||
Repo: "mesh-catalog", Modules: []string{"gitea"}}, t.TempDir(), "", GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a change touching a module ran nothing and was not refused")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheGateRunsWhenTheGraphIsTouchedBesideTheRepositorysOwnCheck(t *testing.T) {
|
||||
registry := checkEnvironment(t)
|
||||
judgeRepo, judgeHead := aCheckedRepository(t, aJudge)
|
||||
beside := map[string]Beside{"mesh-controller": {Repository: judgeRepo, Ref: judgeHead}}
|
||||
check := func(files map[string]string, judge string) CheckVerdict {
|
||||
t.Helper()
|
||||
repo, head := aCheckedRepository(t, files)
|
||||
id := fmt.Sprintf("check-gate-%d", time.Now().UnixNano())
|
||||
v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: repo, Ref: head, Owner: "novox",
|
||||
Repo: "mesh-catalog", Number: 9, Paths: []string{"modules/gitea/index.ts"}, Beside: beside,
|
||||
Modules: []string{"gitea"}, Manifests: []string{"modules/gitea/module.json"}, Judge: judge,
|
||||
Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if left := labelled(id); len(left) > 0 {
|
||||
t.Errorf("the check left %d container(s) behind", len(left))
|
||||
}
|
||||
return v
|
||||
}
|
||||
v := check(map[string]string{"modules/gitea/module.json": `{"module":"gitea"}`, CheckScript: "echo own; exit 0\n"}, "")
|
||||
if v.Gate == nil || v.Gate.Verdict != "warning" || v.Gate.Summary != "a merge rebuilds 14 module(s)" ||
|
||||
strings.Join(v.Gate.Modules, ",") != "gitea" || v.Verdict != "warning" {
|
||||
t.Fatalf("the gate answered %+v\n%s", v.Gate, v.Report)
|
||||
}
|
||||
if v.Repo == nil || v.Repo.Verdict != "pass" {
|
||||
t.Fatalf("its own check answered %+v", v.Repo)
|
||||
}
|
||||
|
||||
v = check(map[string]string{"modules/gitea/module.json": `{"module":"gitea","broken":true}`}, "")
|
||||
if v.Gate.Verdict != "fail" || !strings.Contains(v.Gate.Summary, "module check") || v.Repo.Verdict != "warning" {
|
||||
t.Fatalf("a manifest the judge refuses answered %+v / %+v\n%s", v.Gate, v.Repo, v.Report)
|
||||
}
|
||||
|
||||
// A fault the base branch already had is said and fails nothing; one the change brings fails.
|
||||
repo, _ := aCheckedRepository(t, map[string]string{"modules/gitea/module.json": `{"module":"gitea","broken":true}`})
|
||||
git := func(args ...string) string {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = repo
|
||||
cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.org",
|
||||
"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.org")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git %v: %v\n%s", args, err, out)
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
git("checkout", "--quiet", "-b", "change")
|
||||
if err := os.WriteFile(filepath.Join(repo, "modules/gitea/index.ts"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
git("add", "-A")
|
||||
git("commit", "--quiet", "-m", "y")
|
||||
id := fmt.Sprintf("check-base-%d", time.Now().UnixNano())
|
||||
v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: repo, Ref: git("rev-parse", "HEAD"), Owner: "novox",
|
||||
Repo: "mesh-catalog", Base: "main", Paths: []string{"modules/gitea/index.ts"}, Beside: beside,
|
||||
Modules: []string{"gitea"}, Manifests: []string{"modules/gitea/module.json"}, Toolchain: goToolchain},
|
||||
t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Gate.Verdict != "warning" || !strings.Contains(v.Report, "already") {
|
||||
t.Fatalf("a fault the base already had failed the change: %+v\n%s", v.Gate, v.Report)
|
||||
}
|
||||
|
||||
// A delivery group (novox/hq ADR 0239): the other heads cloned beside it at their heads and handed to the
|
||||
// gate to compose with this one; the repository's own check is each pull request's, not the group's.
|
||||
app, appHead := aCheckedRepository(t, map[string]string{"module.json": `{"module":"app"}`})
|
||||
repo2, head2 := aCheckedRepository(t, map[string]string{"modules/gitea/module.json": `{"module":"gitea"}`,
|
||||
CheckScript: "exit 1\n"})
|
||||
gid := fmt.Sprintf("check-group-%d", time.Now().UnixNano())
|
||||
v, err = Check(t.Context(), Command, CheckSpec{ID: gid, Repository: repo2, Ref: head2, Owner: "novox",
|
||||
Repo: "mesh-catalog", Paths: []string{"modules/gitea/module.json"}, Beside: beside, Modules: []string{"gitea"},
|
||||
Manifests: []string{"modules/gitea/module.json"}, Toolchain: goToolchain,
|
||||
Group: []GroupHead{{Owner: "novox", Repo: "app", Repository: app, Ref: appHead, Paths: []string{"module.json"}}}},
|
||||
t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Gate.Verdict != "pass" || v.Gate.Summary != "composed with novox/app" {
|
||||
t.Fatalf("the group's other head was not composed: %+v\n%s", v.Gate, v.Report)
|
||||
}
|
||||
if v.Repo == nil || v.Repo.Verdict != "pass" || !strings.Contains(v.Repo.Summary, "group") {
|
||||
t.Fatalf("a group's check ran a member's own script: %+v", v.Repo)
|
||||
}
|
||||
|
||||
// A change to the controller judges itself: one that does not build fails its own gate.
|
||||
v = check(map[string]string{"go.mod": "module x\n\ngo 1.22\n", "cmd/mesh-controller/main.go": "package main\nfunc main() { nope }\n",
|
||||
"modules/gitea/module.json": "{}"}, "self")
|
||||
if v.Gate.Verdict != "fail" || !strings.Contains(v.Gate.Summary, "does not build") {
|
||||
t.Fatalf("a controller that does not build judged itself %+v", v.Gate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABuildSaysWhetherItsCommitIsOnTheTrunk(t *testing.T) {
|
||||
repo, onMain := aCheckedRepository(t, map[string]string{"module.json": `{"module":"x","version":"1"}`})
|
||||
git := func(dir string, args ...string) string {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.org",
|
||||
"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.org")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git %v: %v\n%s", args, err, out)
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
git(repo, "checkout", "--quiet", "-b", "feature")
|
||||
if err := os.WriteFile(filepath.Join(repo, "x"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
git(repo, "add", "-A")
|
||||
git(repo, "commit", "--quiet", "-m", "off the trunk")
|
||||
offMain := git(repo, "rev-parse", "HEAD")
|
||||
git(repo, "checkout", "--quiet", "main")
|
||||
|
||||
clone := filepath.Join(t.TempDir(), "clone")
|
||||
git(filepath.Dir(clone), "clone", "--quiet", repo, clone)
|
||||
if trunk, on := trunkOf(t.Context(), Command, clone, onMain); trunk != "main" || !on {
|
||||
t.Errorf("a commit on main reads as on %q: %v", trunk, on)
|
||||
}
|
||||
if trunk, on := trunkOf(t.Context(), Command, clone, offMain); trunk != "main" || on {
|
||||
t.Errorf("a feature branch's commit reads as on %q: %v", trunk, on)
|
||||
}
|
||||
if got := strings.Join(branchesHolding(t.Context(), Command, clone, offMain), ","); got != "feature" {
|
||||
t.Errorf("the feature branch's commit is said to be on %q", got)
|
||||
}
|
||||
if got := strings.Join(branchesHolding(t.Context(), Command, clone, onMain), ","); got != "feature,main" {
|
||||
t.Errorf("main's commit is said to be on %q", got)
|
||||
}
|
||||
// A tree that says no trunk is not known — never read as on it.
|
||||
if trunk, on := trunkOf(t.Context(), Command, repo, onMain); trunk != "" || on {
|
||||
t.Errorf("a repository with no origin reads as trunk %q, on %v", trunk, on)
|
||||
}
|
||||
}
|
||||
|
||||
func TestARedeliveredCheckRemovesWhatItsEarlierDeliveryLeft(t *testing.T) {
|
||||
registry := checkEnvironment(t)
|
||||
repo, head := aCheckedRepository(t, map[string]string{CheckScript: "echo checked\n"})
|
||||
id := fmt.Sprintf("check-again-%d", time.Now().UnixNano())
|
||||
if out, err := exec.Command("docker", "run", "-d", "--rm", "--label", BuildLabel+"="+id, "--name", id+"-store",
|
||||
"postgres:17-alpine", "sleep", "300").CombinedOutput(); err != nil {
|
||||
t.Skipf("no container for the earlier delivery: %v %s", err, out)
|
||||
}
|
||||
v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: repo, Ref: head, Owner: "novox",
|
||||
Repo: "hq", Number: 7, Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("a redelivered check could not run: %v", err)
|
||||
}
|
||||
if v.Repo == nil || v.Repo.Verdict != "pass" {
|
||||
t.Errorf("the repository's check answered %+v", v.Repo)
|
||||
}
|
||||
if left := labelled(id); len(left) > 0 {
|
||||
t.Errorf("the check left %d container(s) behind", len(left))
|
||||
}
|
||||
}
|
||||
|
||||
func TestARepositoryInTwoLanguagesIsCheckedInBoth(t *testing.T) {
|
||||
script := "#!/bin/sh\n# mesh-check-toolchain: typescript\n# mesh-check-also: go replays/merge-check.sh\nnpm test\n"
|
||||
parts := ScriptParts([]byte(script))
|
||||
if len(parts) != 2 || parts[0] != (ScriptPart{"typescript", CheckScript}) ||
|
||||
parts[1] != (ScriptPart{"go", "replays/merge-check.sh"}) {
|
||||
t.Fatalf("the parts read as %+v", parts)
|
||||
}
|
||||
if got := ScriptParts([]byte("#!/bin/sh\nset -eu\n")); len(got) != 1 || got[0] != (ScriptPart{"go", CheckScript}) {
|
||||
t.Fatalf("a script declaring nothing reads as %+v", got)
|
||||
}
|
||||
|
||||
// Each part run where its toolchain is: here, the image the part was given is what the command says.
|
||||
held := CheckSpec{Toolchain: "go-image", Toolchains: map[string]string{"typescript": "ts-image", "go": "go-image"}}
|
||||
run := func(t *testing.T, spec CheckSpec, files map[string]string, parts []ScriptPart) (*Layer, []string) {
|
||||
t.Helper()
|
||||
tree := t.TempDir()
|
||||
for name, body := range files {
|
||||
if err := os.MkdirAll(filepath.Dir(filepath.Join(tree, name)), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tree, name), []byte(body), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
var images []string
|
||||
var out tail
|
||||
layer := ownCheck(t.Context(), spec, parts, tree, &out, func() bool { return false },
|
||||
func(image, script string) *exec.Cmd {
|
||||
images = append(images, image+" "+script)
|
||||
cmd := exec.CommandContext(t.Context(), "sh", script)
|
||||
cmd.Dir = tree
|
||||
return cmd
|
||||
}, func(string, string, ...any) {})
|
||||
return layer, images
|
||||
}
|
||||
twoParts := []ScriptPart{{"typescript", CheckScript}, {"go", "replays/merge-check.sh"}}
|
||||
|
||||
layer, images := run(t, held, map[string]string{CheckScript: "exit 0\n", "replays/merge-check.sh": "exit 0\n"}, twoParts)
|
||||
if layer.Verdict != "pass" || !strings.Contains(layer.Summary, "replays/merge-check.sh (go)") ||
|
||||
strings.Join(images, ", ") != "ts-image merge-check.sh, go-image replays/merge-check.sh" {
|
||||
t.Errorf("both parts passing answered %+v, ran %v", layer, images)
|
||||
}
|
||||
layer, _ = run(t, held, map[string]string{CheckScript: "exit 0\n",
|
||||
"replays/merge-check.sh": "echo 'not gofmt'\"'\"'d:'; echo register.go; exit 1\n"}, twoParts)
|
||||
if layer.Verdict != "fail" || !strings.Contains(layer.Summary, "replays/merge-check.sh failed") ||
|
||||
!strings.Contains(layer.Summary, "register.go") || !strings.Contains(layer.Summary, "merge-check.sh (typescript) passed") {
|
||||
t.Errorf("a failing Go part answered %+v", layer)
|
||||
}
|
||||
layer, images = run(t, held, map[string]string{CheckScript: "exit 2\n", "replays/merge-check.sh": "exit 0\n"}, twoParts)
|
||||
if layer.Verdict != "fail" || len(images) != 1 {
|
||||
t.Errorf("a failing first part answered %+v and ran %v", layer, images)
|
||||
}
|
||||
layer, _ = run(t, held, map[string]string{CheckScript: "exit 0\n"}, twoParts)
|
||||
if layer.Verdict != "fail" || !strings.Contains(layer.Summary, "does not hold") {
|
||||
t.Errorf("a declared part the repository lacks answered %+v", layer)
|
||||
}
|
||||
layer, _ = run(t, held, map[string]string{CheckScript: "exit 0\n"}, []ScriptPart{{"typescript", CheckScript},
|
||||
{"go", "../elsewhere.sh"}})
|
||||
if layer.Verdict != "fail" || !strings.Contains(layer.Summary, "not a path inside") {
|
||||
t.Errorf("a part outside the repository answered %+v", layer)
|
||||
}
|
||||
layer, _ = run(t, CheckSpec{Toolchains: map[string]string{"typescript": "ts-image"}},
|
||||
map[string]string{CheckScript: "exit 0\n", "replays/merge-check.sh": "exit 0\n"}, twoParts)
|
||||
if layer.Verdict != "error" || !strings.Contains(layer.Summary, "go toolchain") {
|
||||
t.Errorf("a part in a toolchain the mesh does not hold answered %+v — never a pass", layer)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -35,69 +32,8 @@ func TestTheStoreAndBusAChecksStandsOnAreTheOnesTheMeshRuns(t *testing.T) {
|
||||
|
||||
// **Issue 306**: the throwaway store is raised without durability — it lives for one check — so a suite
|
||||
// that makes a database per test does not wait on the disk of a busy machine for every flush.
|
||||
func TestTheThrowawayStoreFlushesNothing(t *testing.T) {
|
||||
said := strings.Join(StoreSettings, " ")
|
||||
for _, want := range []string{"-c fsync=off", "-c synchronous_commit=off", "-c full_page_writes=off"} {
|
||||
if !strings.Contains(said, want) {
|
||||
t.Errorf("the store is raised with %q, not %q", said, want)
|
||||
}
|
||||
}
|
||||
if os.Getenv("MESH_TEST_DOCKER") != "1" {
|
||||
t.Skip("MESH_TEST_DOCKER=1: the settings are proven on a raised store")
|
||||
}
|
||||
id := fmt.Sprintf("check-store-%d", time.Now().UnixNano())
|
||||
labelledRun := Labelled(Command, id)
|
||||
t.Cleanup(func() { _, _ = RemoveContainersOf(context.Background(), Command, id) })
|
||||
if _, err := throwaway(t.Context(), labelledRun, Command, id+"-store", StoreImage("17.11"), 5432,
|
||||
[]string{"-e", "POSTGRES_PASSWORD=check"}, StoreSettings); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := waitFor(t.Context(), Command, id+"-store", []string{"pg_isready", "-U", "postgres"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for setting, want := range map[string]string{"fsync": "off", "synchronous_commit": "off", "full_page_writes": "off"} {
|
||||
var got string
|
||||
for try := 0; try < 10; try++ {
|
||||
out, err := exec.Command("docker", "exec", id+"-store", "psql", "-U", "postgres", "-tAc", "SHOW "+setting).CombinedOutput()
|
||||
if got = strings.TrimSpace(string(out)); err == nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("%s is %q on the raised store", setting, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// aRepository is a git repository holding these files, committed, and its head.
|
||||
func aCheckedRepository(t *testing.T, files map[string]string) (string, string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
git := func(args ...string) string {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.org",
|
||||
"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.org")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git %v: %v\n%s", args, err, out)
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
git("init", "--quiet", "-b", "main")
|
||||
for name, body := range files {
|
||||
if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, name)), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
git("add", "-A")
|
||||
git("commit", "--quiet", "-m", "x")
|
||||
return dir, git("rev-parse", "HEAD")
|
||||
}
|
||||
|
||||
func checkEnvironment(t *testing.T) string {
|
||||
t.Helper()
|
||||
@@ -121,104 +57,8 @@ func checkEnvironment(t *testing.T) string {
|
||||
// goToolchain is the Go image a check's script runs in here: the base the controller's own image is built on.
|
||||
const goToolchain = "golang@sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c"
|
||||
|
||||
func labelled(id string) []string {
|
||||
out, _ := exec.Command("docker", "ps", "-aq", "--filter", "label="+BuildLabel+"="+id).Output()
|
||||
return strings.Fields(string(out))
|
||||
}
|
||||
|
||||
func TestACheckRunsTheRepositorysOwnScriptBesideTheMeshsVersionsAndLeavesNothing(t *testing.T) {
|
||||
registry := checkEnvironment(t)
|
||||
// The script proves what it was given: the facts, a store that answers, a bus that answers — and no
|
||||
// container runtime socket.
|
||||
script := `set -e
|
||||
test -s "$MESH_FACTS"
|
||||
grep -q '"bus": "2.11.17"' "$MESH_FACTS" || grep -q '"bus":"2.11.17"' "$MESH_FACTS"
|
||||
case "$MESH_TEST_POSTGRES" in postgres://*127.0.0.1:*) ;; *) echo "no store: $MESH_TEST_POSTGRES"; exit 1;; esac
|
||||
case "$MESH_TEST_NATS" in nats://127.0.0.1:*) ;; *) echo "no bus: $MESH_TEST_NATS"; exit 1;; esac
|
||||
test "$MESH_CHECK_REPOSITORY" = "novox/hq"
|
||||
test "$MESH_CHECK_CHANGED" = "a.go,b.go"
|
||||
test ! -S /var/run/docker.sock || { echo "the check holds the container runtime's socket"; exit 1; }
|
||||
echo checked
|
||||
`
|
||||
repo, head := aCheckedRepository(t, map[string]string{CheckScript: script})
|
||||
id := fmt.Sprintf("check-test-%d", time.Now().UnixNano())
|
||||
v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: repo, Ref: head, Owner: "novox",
|
||||
Repo: "hq", Number: 7, Paths: []string{"a.go", "b.go"}, Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Nothing of the graph touched: the gate a pass that says so, the repository's own check run.
|
||||
if v.Gate == nil || v.Gate.Verdict != "pass" || v.Gate.Summary != noModule {
|
||||
t.Errorf("the gate answered %+v", v.Gate)
|
||||
}
|
||||
if v.Repo == nil || v.Repo.Verdict != "pass" || !strings.Contains(v.Report, "checked") {
|
||||
t.Fatalf("the repository's check answered %+v\n%s", v.Repo, v.Report)
|
||||
}
|
||||
if left := labelled(id); len(left) > 0 {
|
||||
t.Errorf("the check left %d container(s) behind", len(left))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAFailingCheckFailsAndOneThatCannotRunIsNeverAPass(t *testing.T) {
|
||||
registry := checkEnvironment(t)
|
||||
repo, head := aCheckedRepository(t, map[string]string{CheckScript: "echo 'resource \"x.service\": refused'; exit 3\n"})
|
||||
v, err := Check(t.Context(), Command, CheckSpec{ID: fmt.Sprintf("check-fail-%d", time.Now().UnixNano()),
|
||||
Repository: repo, Ref: head, Owner: "novox", Repo: "hq", Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Repo == nil || v.Repo.Verdict != "fail" || !strings.Contains(v.Repo.Summary, "refused") {
|
||||
t.Fatalf("a failing script answered %+v", v.Repo)
|
||||
}
|
||||
|
||||
// A script in a toolchain the mesh does not hold: an error, never a pass.
|
||||
repo, head = aCheckedRepository(t, map[string]string{CheckScript: "# mesh-check-toolchain: cobol\nexit 0\n"})
|
||||
v, err = Check(t.Context(), Command, CheckSpec{ID: fmt.Sprintf("check-cobol-%d", time.Now().UnixNano()),
|
||||
Repository: repo, Ref: head, Owner: "novox", Repo: "hq", Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err != nil || v.Repo == nil || v.Repo.Verdict != "error" {
|
||||
t.Fatalf("a script in a toolchain nobody holds answered %+v, %v", v.Repo, err)
|
||||
}
|
||||
|
||||
// Past its bound: an error, not a pass.
|
||||
was := CheckTimeout
|
||||
CheckTimeout = 25 * time.Second
|
||||
t.Cleanup(func() { CheckTimeout = was })
|
||||
repo, head = aCheckedRepository(t, map[string]string{CheckScript: "sleep 120\n"})
|
||||
v, err = Check(context.Background(), Command, CheckSpec{ID: fmt.Sprintf("check-slow-%d", time.Now().UnixNano()),
|
||||
Repository: repo, Ref: head, Owner: "novox", Repo: "hq", Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err == nil && (v.Repo == nil || v.Repo.Verdict != "error") {
|
||||
t.Fatalf("a check past its bound answered %+v", v.Repo)
|
||||
}
|
||||
|
||||
// No facts: it cannot run, and says so.
|
||||
repo, head = aCheckedRepository(t, map[string]string{CheckScript: "exit 0\n"})
|
||||
_, err = Check(t.Context(), Command, CheckSpec{ID: "check-nofacts", Repository: repo, Ref: head, Owner: "novox",
|
||||
Repo: "hq", Toolchain: goToolchain}, t.TempDir(), "127.0.0.1:1", GitCredential{}, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "facts snapshot") {
|
||||
t.Fatalf("a check with no facts said %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A repository that touches nothing of the graph and has no script of its own runs nothing, and says
|
||||
// both: the gate a pass that names why, the repository a warning — never silent.
|
||||
func TestACheckWithNothingToRunSaysSo(t *testing.T) {
|
||||
repo, head := aCheckedRepository(t, map[string]string{"README.md": "x"})
|
||||
v, err := Check(t.Context(), Command, CheckSpec{ID: "check-nothing", Repository: repo, Ref: head, Owner: "novox",
|
||||
Repo: "hq"}, t.TempDir(), "", GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Gate == nil || v.Gate.Verdict != "pass" || v.Repo == nil || v.Repo.Verdict != "warning" ||
|
||||
!strings.Contains(v.Repo.Summary, CheckScript) {
|
||||
t.Fatalf("a check with nothing to run said %+v / %+v", v.Gate, v.Repo)
|
||||
}
|
||||
// A gated change never takes that path: with no store to read the facts from, it cannot run.
|
||||
_, err = Check(t.Context(), Command, CheckSpec{ID: "check-gated", Repository: repo, Ref: head, Owner: "novox",
|
||||
Repo: "mesh-catalog", Modules: []string{"gitea"}}, t.TempDir(), "", GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a change touching a module ran nothing and was not refused")
|
||||
}
|
||||
}
|
||||
|
||||
// A script declares its toolchain among its first lines; go when it declares none.
|
||||
func TestAScriptDeclaresItsToolchain(t *testing.T) {
|
||||
@@ -314,143 +154,8 @@ func main() {
|
||||
`,
|
||||
}
|
||||
|
||||
func TestTheGateRunsWhenTheGraphIsTouchedBesideTheRepositorysOwnCheck(t *testing.T) {
|
||||
registry := checkEnvironment(t)
|
||||
judgeRepo, judgeHead := aCheckedRepository(t, aJudge)
|
||||
beside := map[string]Beside{"mesh-controller": {Repository: judgeRepo, Ref: judgeHead}}
|
||||
check := func(files map[string]string, judge string) CheckVerdict {
|
||||
t.Helper()
|
||||
repo, head := aCheckedRepository(t, files)
|
||||
id := fmt.Sprintf("check-gate-%d", time.Now().UnixNano())
|
||||
v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: repo, Ref: head, Owner: "novox",
|
||||
Repo: "mesh-catalog", Number: 9, Paths: []string{"modules/gitea/index.ts"}, Beside: beside,
|
||||
Modules: []string{"gitea"}, Manifests: []string{"modules/gitea/module.json"}, Judge: judge,
|
||||
Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if left := labelled(id); len(left) > 0 {
|
||||
t.Errorf("the check left %d container(s) behind", len(left))
|
||||
}
|
||||
return v
|
||||
}
|
||||
v := check(map[string]string{"modules/gitea/module.json": `{"module":"gitea"}`, CheckScript: "echo own; exit 0\n"}, "")
|
||||
if v.Gate == nil || v.Gate.Verdict != "warning" || v.Gate.Summary != "a merge rebuilds 14 module(s)" ||
|
||||
strings.Join(v.Gate.Modules, ",") != "gitea" || v.Verdict != "warning" {
|
||||
t.Fatalf("the gate answered %+v\n%s", v.Gate, v.Report)
|
||||
}
|
||||
if v.Repo == nil || v.Repo.Verdict != "pass" {
|
||||
t.Fatalf("its own check answered %+v", v.Repo)
|
||||
}
|
||||
|
||||
v = check(map[string]string{"modules/gitea/module.json": `{"module":"gitea","broken":true}`}, "")
|
||||
if v.Gate.Verdict != "fail" || !strings.Contains(v.Gate.Summary, "module check") || v.Repo.Verdict != "warning" {
|
||||
t.Fatalf("a manifest the judge refuses answered %+v / %+v\n%s", v.Gate, v.Repo, v.Report)
|
||||
}
|
||||
|
||||
// A fault the base branch already had is said and fails nothing; one the change brings fails.
|
||||
repo, _ := aCheckedRepository(t, map[string]string{"modules/gitea/module.json": `{"module":"gitea","broken":true}`})
|
||||
git := func(args ...string) string {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = repo
|
||||
cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.org",
|
||||
"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.org")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git %v: %v\n%s", args, err, out)
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
git("checkout", "--quiet", "-b", "change")
|
||||
if err := os.WriteFile(filepath.Join(repo, "modules/gitea/index.ts"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
git("add", "-A")
|
||||
git("commit", "--quiet", "-m", "y")
|
||||
id := fmt.Sprintf("check-base-%d", time.Now().UnixNano())
|
||||
v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: repo, Ref: git("rev-parse", "HEAD"), Owner: "novox",
|
||||
Repo: "mesh-catalog", Base: "main", Paths: []string{"modules/gitea/index.ts"}, Beside: beside,
|
||||
Modules: []string{"gitea"}, Manifests: []string{"modules/gitea/module.json"}, Toolchain: goToolchain},
|
||||
t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Gate.Verdict != "warning" || !strings.Contains(v.Report, "already") {
|
||||
t.Fatalf("a fault the base already had failed the change: %+v\n%s", v.Gate, v.Report)
|
||||
}
|
||||
|
||||
// A delivery group (novox/hq ADR 0239): the other heads cloned beside it at their heads and handed to the
|
||||
// gate to compose with this one; the repository's own check is each pull request's, not the group's.
|
||||
app, appHead := aCheckedRepository(t, map[string]string{"module.json": `{"module":"app"}`})
|
||||
repo2, head2 := aCheckedRepository(t, map[string]string{"modules/gitea/module.json": `{"module":"gitea"}`,
|
||||
CheckScript: "exit 1\n"})
|
||||
gid := fmt.Sprintf("check-group-%d", time.Now().UnixNano())
|
||||
v, err = Check(t.Context(), Command, CheckSpec{ID: gid, Repository: repo2, Ref: head2, Owner: "novox",
|
||||
Repo: "mesh-catalog", Paths: []string{"modules/gitea/module.json"}, Beside: beside, Modules: []string{"gitea"},
|
||||
Manifests: []string{"modules/gitea/module.json"}, Toolchain: goToolchain,
|
||||
Group: []GroupHead{{Owner: "novox", Repo: "app", Repository: app, Ref: appHead, Paths: []string{"module.json"}}}},
|
||||
t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Gate.Verdict != "pass" || v.Gate.Summary != "composed with novox/app" {
|
||||
t.Fatalf("the group's other head was not composed: %+v\n%s", v.Gate, v.Report)
|
||||
}
|
||||
if v.Repo == nil || v.Repo.Verdict != "pass" || !strings.Contains(v.Repo.Summary, "group") {
|
||||
t.Fatalf("a group's check ran a member's own script: %+v", v.Repo)
|
||||
}
|
||||
|
||||
// A change to the controller judges itself: one that does not build fails its own gate.
|
||||
v = check(map[string]string{"go.mod": "module x\n\ngo 1.22\n", "cmd/mesh-controller/main.go": "package main\nfunc main() { nope }\n",
|
||||
"modules/gitea/module.json": "{}"}, "self")
|
||||
if v.Gate.Verdict != "fail" || !strings.Contains(v.Gate.Summary, "does not build") {
|
||||
t.Fatalf("a controller that does not build judged itself %+v", v.Gate)
|
||||
}
|
||||
}
|
||||
|
||||
// **Only a commit on the trunk is published** (novox/hq ADR 0238): the build seat reads, from the clone it
|
||||
// just made, the branch the forge names its default and whether the commit built is on it.
|
||||
func TestABuildSaysWhetherItsCommitIsOnTheTrunk(t *testing.T) {
|
||||
repo, onMain := aCheckedRepository(t, map[string]string{"module.json": `{"module":"x","version":"1"}`})
|
||||
git := func(dir string, args ...string) string {
|
||||
cmd := exec.Command("git", args...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.org",
|
||||
"GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.org")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git %v: %v\n%s", args, err, out)
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
git(repo, "checkout", "--quiet", "-b", "feature")
|
||||
if err := os.WriteFile(filepath.Join(repo, "x"), []byte("x"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
git(repo, "add", "-A")
|
||||
git(repo, "commit", "--quiet", "-m", "off the trunk")
|
||||
offMain := git(repo, "rev-parse", "HEAD")
|
||||
git(repo, "checkout", "--quiet", "main")
|
||||
|
||||
clone := filepath.Join(t.TempDir(), "clone")
|
||||
git(filepath.Dir(clone), "clone", "--quiet", repo, clone)
|
||||
if trunk, on := trunkOf(t.Context(), Command, clone, onMain); trunk != "main" || !on {
|
||||
t.Errorf("a commit on main reads as on %q: %v", trunk, on)
|
||||
}
|
||||
if trunk, on := trunkOf(t.Context(), Command, clone, offMain); trunk != "main" || on {
|
||||
t.Errorf("a feature branch's commit reads as on %q: %v", trunk, on)
|
||||
}
|
||||
if got := strings.Join(branchesHolding(t.Context(), Command, clone, offMain), ","); got != "feature" {
|
||||
t.Errorf("the feature branch's commit is said to be on %q", got)
|
||||
}
|
||||
if got := strings.Join(branchesHolding(t.Context(), Command, clone, onMain), ","); got != "feature,main" {
|
||||
t.Errorf("main's commit is said to be on %q", got)
|
||||
}
|
||||
// A tree that says no trunk is not known — never read as on it.
|
||||
if trunk, on := trunkOf(t.Context(), Command, repo, onMain); trunk != "" || on {
|
||||
t.Errorf("a repository with no origin reads as trunk %q, on %v", trunk, on)
|
||||
}
|
||||
}
|
||||
|
||||
// **Issue 286**: a failed merge-check.sh was said by its last line — a bare "FAIL", or the name of a file
|
||||
// gofmt listed — which named nothing a reader could act on. The status says what failed.
|
||||
@@ -497,99 +202,12 @@ func TestTheSeatCallsAGateThatRaisedNoMachineAnError(t *testing.T) {
|
||||
|
||||
// **Issue 285**: an ask redelivered after its holder stopped mid-check found its own throwaway store still
|
||||
// there under its name, and the check said it could not run. What an earlier delivery left goes first.
|
||||
func TestARedeliveredCheckRemovesWhatItsEarlierDeliveryLeft(t *testing.T) {
|
||||
registry := checkEnvironment(t)
|
||||
repo, head := aCheckedRepository(t, map[string]string{CheckScript: "echo checked\n"})
|
||||
id := fmt.Sprintf("check-again-%d", time.Now().UnixNano())
|
||||
if out, err := exec.Command("docker", "run", "-d", "--rm", "--label", BuildLabel+"="+id, "--name", id+"-store",
|
||||
"postgres:17-alpine", "sleep", "300").CombinedOutput(); err != nil {
|
||||
t.Skipf("no container for the earlier delivery: %v %s", err, out)
|
||||
}
|
||||
v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: repo, Ref: head, Owner: "novox",
|
||||
Repo: "hq", Number: 7, Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("a redelivered check could not run: %v", err)
|
||||
}
|
||||
if v.Repo == nil || v.Repo.Verdict != "pass" {
|
||||
t.Errorf("the repository's check answered %+v", v.Repo)
|
||||
}
|
||||
if left := labelled(id); len(left) > 0 {
|
||||
t.Errorf("the check left %d container(s) behind", len(left))
|
||||
}
|
||||
}
|
||||
|
||||
// **A repository in two languages checks both, each in its own toolchain** (novox/hq issue 302): the lab's
|
||||
// merge-check.sh runs in the TypeScript toolchain, and its Go replays were said "NOT CHECKED HERE" on every
|
||||
// pull request, so a register that did not compile would have merged green. Its script declares the Go
|
||||
// part; the check runs it in the Go toolchain's container after the script, and the layer passes only
|
||||
// when both do.
|
||||
func TestARepositoryInTwoLanguagesIsCheckedInBoth(t *testing.T) {
|
||||
script := "#!/bin/sh\n# mesh-check-toolchain: typescript\n# mesh-check-also: go replays/merge-check.sh\nnpm test\n"
|
||||
parts := ScriptParts([]byte(script))
|
||||
if len(parts) != 2 || parts[0] != (ScriptPart{"typescript", CheckScript}) ||
|
||||
parts[1] != (ScriptPart{"go", "replays/merge-check.sh"}) {
|
||||
t.Fatalf("the parts read as %+v", parts)
|
||||
}
|
||||
if got := ScriptParts([]byte("#!/bin/sh\nset -eu\n")); len(got) != 1 || got[0] != (ScriptPart{"go", CheckScript}) {
|
||||
t.Fatalf("a script declaring nothing reads as %+v", got)
|
||||
}
|
||||
|
||||
// Each part run where its toolchain is: here, the image the part was given is what the command says.
|
||||
held := CheckSpec{Toolchain: "go-image", Toolchains: map[string]string{"typescript": "ts-image", "go": "go-image"}}
|
||||
run := func(t *testing.T, spec CheckSpec, files map[string]string, parts []ScriptPart) (*Layer, []string) {
|
||||
t.Helper()
|
||||
tree := t.TempDir()
|
||||
for name, body := range files {
|
||||
if err := os.MkdirAll(filepath.Dir(filepath.Join(tree, name)), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tree, name), []byte(body), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
var images []string
|
||||
var out tail
|
||||
layer := ownCheck(t.Context(), spec, parts, tree, &out, func() bool { return false },
|
||||
func(image, script string) *exec.Cmd {
|
||||
images = append(images, image+" "+script)
|
||||
cmd := exec.CommandContext(t.Context(), "sh", script)
|
||||
cmd.Dir = tree
|
||||
return cmd
|
||||
}, func(string, string, ...any) {})
|
||||
return layer, images
|
||||
}
|
||||
twoParts := []ScriptPart{{"typescript", CheckScript}, {"go", "replays/merge-check.sh"}}
|
||||
|
||||
layer, images := run(t, held, map[string]string{CheckScript: "exit 0\n", "replays/merge-check.sh": "exit 0\n"}, twoParts)
|
||||
if layer.Verdict != "pass" || !strings.Contains(layer.Summary, "replays/merge-check.sh (go)") ||
|
||||
strings.Join(images, ", ") != "ts-image merge-check.sh, go-image replays/merge-check.sh" {
|
||||
t.Errorf("both parts passing answered %+v, ran %v", layer, images)
|
||||
}
|
||||
layer, _ = run(t, held, map[string]string{CheckScript: "exit 0\n",
|
||||
"replays/merge-check.sh": "echo 'not gofmt'\"'\"'d:'; echo register.go; exit 1\n"}, twoParts)
|
||||
if layer.Verdict != "fail" || !strings.Contains(layer.Summary, "replays/merge-check.sh failed") ||
|
||||
!strings.Contains(layer.Summary, "register.go") || !strings.Contains(layer.Summary, "merge-check.sh (typescript) passed") {
|
||||
t.Errorf("a failing Go part answered %+v", layer)
|
||||
}
|
||||
layer, images = run(t, held, map[string]string{CheckScript: "exit 2\n", "replays/merge-check.sh": "exit 0\n"}, twoParts)
|
||||
if layer.Verdict != "fail" || len(images) != 1 {
|
||||
t.Errorf("a failing first part answered %+v and ran %v", layer, images)
|
||||
}
|
||||
layer, _ = run(t, held, map[string]string{CheckScript: "exit 0\n"}, twoParts)
|
||||
if layer.Verdict != "fail" || !strings.Contains(layer.Summary, "does not hold") {
|
||||
t.Errorf("a declared part the repository lacks answered %+v", layer)
|
||||
}
|
||||
layer, _ = run(t, held, map[string]string{CheckScript: "exit 0\n"}, []ScriptPart{{"typescript", CheckScript},
|
||||
{"go", "../elsewhere.sh"}})
|
||||
if layer.Verdict != "fail" || !strings.Contains(layer.Summary, "not a path inside") {
|
||||
t.Errorf("a part outside the repository answered %+v", layer)
|
||||
}
|
||||
layer, _ = run(t, CheckSpec{Toolchains: map[string]string{"typescript": "ts-image"}},
|
||||
map[string]string{CheckScript: "exit 0\n", "replays/merge-check.sh": "exit 0\n"}, twoParts)
|
||||
if layer.Verdict != "error" || !strings.Contains(layer.Summary, "go toolchain") {
|
||||
t.Errorf("a part in a toolchain the mesh does not hold answered %+v — never a pass", layer)
|
||||
}
|
||||
}
|
||||
|
||||
// A branch tried before its pull request may narrow what the repository's script runs (novox/hq issue 686); a
|
||||
// pull request's check is handed nothing, and runs whole.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func ownCheckOf(t *testing.T, printed string) (*Layer, []string) {
|
||||
t.Helper()
|
||||
tree := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(tree, "printed.txt"), []byte(printed), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tree, CheckScript), []byte("cat printed.txt\nexit 1\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out tail
|
||||
var logged []string
|
||||
layer := ownCheck(t.Context(), CheckSpec{Toolchain: "go-image"}, []ScriptPart{{"go", CheckScript}}, tree, &out,
|
||||
func() bool { return false },
|
||||
func(_, script string) *exec.Cmd {
|
||||
cmd := exec.CommandContext(t.Context(), "sh", script)
|
||||
cmd.Dir = tree
|
||||
return cmd
|
||||
}, func(step, format string, args ...any) {
|
||||
if step == "output" {
|
||||
logged = append(logged, fmt.Sprintf(format, args...))
|
||||
}
|
||||
})
|
||||
if layer == nil || layer.Verdict != "fail" {
|
||||
t.Fatalf("a failing check answered %+v", layer)
|
||||
}
|
||||
return layer, logged
|
||||
}
|
||||
|
||||
func TestAFailureEarlyInALongRunIsStillNamed(t *testing.T) {
|
||||
layer, _ := ownCheckOf(t, captured(t, "a-long-run-failing-early.txt"))
|
||||
if !strings.Contains(layer.Summary, "failed: --- FAIL: TestTheEnvelopeIsPinned (0.00s)") {
|
||||
t.Errorf("a failure 600 lines from the end is said as %q", layer.Summary)
|
||||
}
|
||||
if !strings.Contains(layer.Summary, "TestSubtests/the_hub") {
|
||||
t.Errorf("the second failing test is not named: %q", layer.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestARaceRunOfEveryPackageNamesItsFirstFailure(t *testing.T) {
|
||||
layer, _ := ownCheckOf(t, captured(t, "a-race-run-of-every-package.txt"))
|
||||
if !strings.Contains(layer.Summary, "failed: --- FAIL: TestTheEnvelopeIsPinned (0.00s)") ||
|
||||
!strings.Contains(layer.Summary, "TestAMapIsNil") || !strings.Contains(layer.Summary, "TestACounterIsShared") {
|
||||
t.Errorf("a run failing in four packages is said as %q", layer.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABuildErrorIsNamedByTheError(t *testing.T) {
|
||||
layer, _ := ownCheckOf(t, captured(t, "a-build-error.txt"))
|
||||
if !strings.HasSuffix(layer.Summary, "failed: broken/broken.go:3:28: undefined: undefinedThing") {
|
||||
t.Errorf("a build error is said as %q", layer.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheWholeOutputIsInTheBuildsLog(t *testing.T) {
|
||||
printed := captured(t, "a-long-run-failing-early.txt")
|
||||
_, logged := ownCheckOf(t, printed)
|
||||
want := strings.Split(strings.TrimRight(printed, "\n"), "\n")
|
||||
if len(logged) != len(want) {
|
||||
t.Fatalf("the build's log holds %d line(s) of the %d printed", len(logged), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if logged[i] != want[i] {
|
||||
t.Fatalf("line %d is logged as %q, printed as %q", i+1, logged[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheVerdictKeepsWhatFailedWhole(t *testing.T) {
|
||||
layer, _ := ownCheckOf(t, captured(t, "a-long-run-failing-early.txt"))
|
||||
want := "--- FAIL: TestTheEnvelopeIsPinned (0.00s)\n" +
|
||||
" early_test.go:9: the envelope's subject is \"mesh.a\", not \"mesh.b\"\n" +
|
||||
" early_test.go:10: a second line of the same failure\n" +
|
||||
"--- FAIL: TestSubtests (0.00s)\n" +
|
||||
" --- FAIL: TestSubtests/the_hub (0.00s)\n" +
|
||||
" early_test.go:14: the hub does not compose\n" +
|
||||
"FAIL\texample.com/cap/early\t"
|
||||
if !strings.HasPrefix(layer.Failed, want) || strings.Contains(layer.Failed, "kept what home-server says") {
|
||||
t.Errorf("the early failure is kept as:\n%s", layer.Failed)
|
||||
}
|
||||
|
||||
layer, _ = ownCheckOf(t, captured(t, "a-panic.txt"))
|
||||
for _, line := range []string{"--- FAIL: TestAMapIsNil (0.00s)", "panic: assignment to entry in nil map",
|
||||
"[running]:", "example.com/cap/panics.TestAMapIsNil(", "/src/cap/panics/panics_test.go:7",
|
||||
"FAIL\texample.com/cap/panics\t"} {
|
||||
if !strings.Contains(layer.Failed, line) {
|
||||
t.Errorf("a panic is kept without %q:\n%s", line, layer.Failed)
|
||||
}
|
||||
}
|
||||
|
||||
layer, _ = ownCheckOf(t, captured(t, "a-data-race.txt"))
|
||||
race := captured(t, "a-data-race.txt")
|
||||
report := race[:strings.Index(race, "--- FAIL:")]
|
||||
if !strings.HasPrefix(layer.Failed, report) ||
|
||||
!strings.Contains(layer.Failed, "--- FAIL: TestACounterIsShared (0.00s)\n testing.go:1865: race detected") {
|
||||
t.Errorf("a data race is kept as:\n%s", layer.Failed)
|
||||
}
|
||||
|
||||
layer, _ = ownCheckOf(t, captured(t, "a-build-error.txt"))
|
||||
if layer.Failed != "# example.com/cap/broken\nbroken/broken.go:3:28: undefined: undefinedThing\n"+
|
||||
"FAIL\texample.com/cap/broken [build failed]" {
|
||||
t.Errorf("a build error is kept as:\n%s", layer.Failed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhatFailedIsBoundedAndSurvivesTheReportsCut(t *testing.T) {
|
||||
var b strings.Builder
|
||||
for i := range 3000 {
|
||||
fmt.Fprintf(&b, "--- FAIL: TestNumber%d (0.00s)\n x_test.go:1: %s\n", i, strings.Repeat("why ", 600))
|
||||
fmt.Fprintf(&b, "2026/10/11 01:30:03 a log line %d\n", i)
|
||||
}
|
||||
b.WriteString("FAIL\tx/everything\t1s\nFAIL\n")
|
||||
layer, logged := ownCheckOf(t, b.String())
|
||||
if len(layer.Failed) > failureBytes+2*failureLineBytes || !strings.Contains(layer.Failed, "more line(s) naming what failed") {
|
||||
t.Errorf("what failed in a run that fails everywhere is %d bytes, ending %q", len(layer.Failed),
|
||||
layer.Failed[max(0, len(layer.Failed)-200):])
|
||||
}
|
||||
if !strings.Contains(layer.Summary, "--- FAIL: TestNumber0 (0.00s); and TestNumber1, ") ||
|
||||
!strings.Contains(layer.Summary, "and 2994 more") {
|
||||
t.Errorf("a run failing 3000 tests is said as %q", layer.Summary)
|
||||
}
|
||||
|
||||
var out tail
|
||||
out.Write([]byte(b.String()))
|
||||
report := withWhatFailed(out.String(), layer)
|
||||
const maxCheckReport = 60 << 10 // as the controller cuts it, from the front
|
||||
if len(report) > maxCheckReport {
|
||||
report = report[len(report)-maxCheckReport:]
|
||||
}
|
||||
if !strings.Contains(report, "--- what failed") || !strings.Contains(report, "--- FAIL: TestNumber0 (0.00s)") {
|
||||
t.Error("what failed did not survive the controller's cut of the report")
|
||||
}
|
||||
|
||||
// The build's log keeps its first logLines lines, says how many it left out, then what failed.
|
||||
if len(logged) < logLines+2 || logged[logLines] != "… 1002 more line(s) of its output are not in this log, which keeps its first 8000" ||
|
||||
logged[logLines+1] != "--- what failed, picked from the whole of its output" ||
|
||||
logged[logLines+2] != "--- FAIL: TestNumber0 (0.00s)" {
|
||||
t.Errorf("the log past its bound says %q", logged[logLines:min(len(logged), logLines+3)])
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,13 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ownCheckOf runs a repository's own check whose script prints a captured output and fails, as the build
|
||||
// seat runs it, and answers the layer and every line it said in the build's log.
|
||||
func ownCheckOf(t *testing.T, printed string) (*Layer, []string) {
|
||||
t.Helper()
|
||||
tree := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(tree, "printed.txt"), []byte(printed), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(tree, CheckScript), []byte("cat printed.txt\nexit 1\n"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var out tail
|
||||
var logged []string
|
||||
layer := ownCheck(t.Context(), CheckSpec{Toolchain: "go-image"}, []ScriptPart{{"go", CheckScript}}, tree, &out,
|
||||
func() bool { return false },
|
||||
func(_, script string) *exec.Cmd {
|
||||
cmd := exec.CommandContext(t.Context(), "sh", script)
|
||||
cmd.Dir = tree
|
||||
return cmd
|
||||
}, func(step, format string, args ...any) {
|
||||
if step == "output" {
|
||||
logged = append(logged, fmt.Sprintf(format, args...))
|
||||
}
|
||||
})
|
||||
if layer == nil || layer.Verdict != "fail" {
|
||||
t.Fatalf("a failing check answered %+v", layer)
|
||||
}
|
||||
return layer, logged
|
||||
}
|
||||
|
||||
func captured(t *testing.T, name string) string {
|
||||
t.Helper()
|
||||
@@ -52,124 +22,18 @@ func captured(t *testing.T, name string) string {
|
||||
// name: the package's own logs, printed after its `--- FAIL:` lines, pushed them out of the last 200 lines,
|
||||
// which was all the summary was named from. The first failing test is named, and the others after it,
|
||||
// however much came after them.
|
||||
func TestAFailureEarlyInALongRunIsStillNamed(t *testing.T) {
|
||||
layer, _ := ownCheckOf(t, captured(t, "a-long-run-failing-early.txt"))
|
||||
if !strings.Contains(layer.Summary, "failed: --- FAIL: TestTheEnvelopeIsPinned (0.00s)") {
|
||||
t.Errorf("a failure 600 lines from the end is said as %q", layer.Summary)
|
||||
}
|
||||
if !strings.Contains(layer.Summary, "TestSubtests/the_hub") {
|
||||
t.Errorf("the second failing test is not named: %q", layer.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
// One -race run of every package: the first failure is the earliest, not the last within the tail.
|
||||
func TestARaceRunOfEveryPackageNamesItsFirstFailure(t *testing.T) {
|
||||
layer, _ := ownCheckOf(t, captured(t, "a-race-run-of-every-package.txt"))
|
||||
if !strings.Contains(layer.Summary, "failed: --- FAIL: TestTheEnvelopeIsPinned (0.00s)") ||
|
||||
!strings.Contains(layer.Summary, "TestAMapIsNil") || !strings.Contains(layer.Summary, "TestACounterIsShared") {
|
||||
t.Errorf("a run failing in four packages is said as %q", layer.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
// A package that does not build is named by its error, not by its bare "[build failed]".
|
||||
func TestABuildErrorIsNamedByTheError(t *testing.T) {
|
||||
layer, _ := ownCheckOf(t, captured(t, "a-build-error.txt"))
|
||||
if !strings.HasSuffix(layer.Summary, "failed: broken/broken.go:3:28: undefined: undefinedThing") {
|
||||
t.Errorf("a build error is said as %q", layer.Summary)
|
||||
}
|
||||
}
|
||||
|
||||
// The whole of what the check printed is in the build's log, line by line — where before none of it was.
|
||||
func TestTheWholeOutputIsInTheBuildsLog(t *testing.T) {
|
||||
printed := captured(t, "a-long-run-failing-early.txt")
|
||||
_, logged := ownCheckOf(t, printed)
|
||||
want := strings.Split(strings.TrimRight(printed, "\n"), "\n")
|
||||
if len(logged) != len(want) {
|
||||
t.Fatalf("the build's log holds %d line(s) of the %d printed", len(logged), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if logged[i] != want[i] {
|
||||
t.Fatalf("line %d is logged as %q, printed as %q", i+1, logged[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// What failed is kept whole in the verdict: each --- FAIL with its messages, a panic with its first frames,
|
||||
// a data race report from rule to rule — and none of the log lines around them.
|
||||
func TestTheVerdictKeepsWhatFailedWhole(t *testing.T) {
|
||||
layer, _ := ownCheckOf(t, captured(t, "a-long-run-failing-early.txt"))
|
||||
want := "--- FAIL: TestTheEnvelopeIsPinned (0.00s)\n" +
|
||||
" early_test.go:9: the envelope's subject is \"mesh.a\", not \"mesh.b\"\n" +
|
||||
" early_test.go:10: a second line of the same failure\n" +
|
||||
"--- FAIL: TestSubtests (0.00s)\n" +
|
||||
" --- FAIL: TestSubtests/the_hub (0.00s)\n" +
|
||||
" early_test.go:14: the hub does not compose\n" +
|
||||
"FAIL\texample.com/cap/early\t"
|
||||
if !strings.HasPrefix(layer.Failed, want) || strings.Contains(layer.Failed, "kept what home-server says") {
|
||||
t.Errorf("the early failure is kept as:\n%s", layer.Failed)
|
||||
}
|
||||
|
||||
layer, _ = ownCheckOf(t, captured(t, "a-panic.txt"))
|
||||
for _, line := range []string{"--- FAIL: TestAMapIsNil (0.00s)", "panic: assignment to entry in nil map",
|
||||
"[running]:", "example.com/cap/panics.TestAMapIsNil(", "/src/cap/panics/panics_test.go:7",
|
||||
"FAIL\texample.com/cap/panics\t"} {
|
||||
if !strings.Contains(layer.Failed, line) {
|
||||
t.Errorf("a panic is kept without %q:\n%s", line, layer.Failed)
|
||||
}
|
||||
}
|
||||
|
||||
layer, _ = ownCheckOf(t, captured(t, "a-data-race.txt"))
|
||||
race := captured(t, "a-data-race.txt")
|
||||
report := race[:strings.Index(race, "--- FAIL:")]
|
||||
if !strings.HasPrefix(layer.Failed, report) ||
|
||||
!strings.Contains(layer.Failed, "--- FAIL: TestACounterIsShared (0.00s)\n testing.go:1865: race detected") {
|
||||
t.Errorf("a data race is kept as:\n%s", layer.Failed)
|
||||
}
|
||||
|
||||
layer, _ = ownCheckOf(t, captured(t, "a-build-error.txt"))
|
||||
if layer.Failed != "# example.com/cap/broken\nbroken/broken.go:3:28: undefined: undefinedThing\n"+
|
||||
"FAIL\texample.com/cap/broken [build failed]" {
|
||||
t.Errorf("a build error is kept as:\n%s", layer.Failed)
|
||||
}
|
||||
}
|
||||
|
||||
// A run that fails everywhere is bounded in what it keeps, and what it keeps survives the controller's cut
|
||||
// of a report to its last 60 KiB, said at the report's end.
|
||||
func TestWhatFailedIsBoundedAndSurvivesTheReportsCut(t *testing.T) {
|
||||
var b strings.Builder
|
||||
for i := range 3000 {
|
||||
fmt.Fprintf(&b, "--- FAIL: TestNumber%d (0.00s)\n x_test.go:1: %s\n", i, strings.Repeat("why ", 600))
|
||||
fmt.Fprintf(&b, "2026/10/11 01:30:03 a log line %d\n", i)
|
||||
}
|
||||
b.WriteString("FAIL\tx/everything\t1s\nFAIL\n")
|
||||
layer, logged := ownCheckOf(t, b.String())
|
||||
if len(layer.Failed) > failureBytes+2*failureLineBytes || !strings.Contains(layer.Failed, "more line(s) naming what failed") {
|
||||
t.Errorf("what failed in a run that fails everywhere is %d bytes, ending %q", len(layer.Failed),
|
||||
layer.Failed[max(0, len(layer.Failed)-200):])
|
||||
}
|
||||
if !strings.Contains(layer.Summary, "--- FAIL: TestNumber0 (0.00s); and TestNumber1, ") ||
|
||||
!strings.Contains(layer.Summary, "and 2994 more") {
|
||||
t.Errorf("a run failing 3000 tests is said as %q", layer.Summary)
|
||||
}
|
||||
|
||||
var out tail
|
||||
out.Write([]byte(b.String()))
|
||||
report := withWhatFailed(out.String(), layer)
|
||||
const maxCheckReport = 60 << 10 // as the controller cuts it, from the front
|
||||
if len(report) > maxCheckReport {
|
||||
report = report[len(report)-maxCheckReport:]
|
||||
}
|
||||
if !strings.Contains(report, "--- what failed") || !strings.Contains(report, "--- FAIL: TestNumber0 (0.00s)") {
|
||||
t.Error("what failed did not survive the controller's cut of the report")
|
||||
}
|
||||
|
||||
// The build's log keeps its first logLines lines, says how many it left out, then what failed.
|
||||
if len(logged) < logLines+2 || logged[logLines] != "… 1002 more line(s) of its output are not in this log, which keeps its first 8000" ||
|
||||
logged[logLines+1] != "--- what failed, picked from the whole of its output" ||
|
||||
logged[logLines+2] != "--- FAIL: TestNumber0 (0.00s)" {
|
||||
t.Errorf("the log past its bound says %q", logged[logLines:min(len(logged), logLines+3)])
|
||||
}
|
||||
}
|
||||
|
||||
// A passing check says nothing of what failed.
|
||||
func TestAPassingCheckReportsAsBefore(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAGoProgramsBuildSourceIsItsImportClosure(t *testing.T) {
|
||||
root := aGoTree(t, aProgram)
|
||||
got, err := GoBuildSource(root, "cmd/prog")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range []struct {
|
||||
file string
|
||||
held bool
|
||||
why string
|
||||
}{
|
||||
{"cmd/prog/main.go", true, "the program itself"},
|
||||
{"cmd/prog/helper.go", true, "a file added to the program's package"},
|
||||
{"lib/lib.go", true, "a package it imports"},
|
||||
{"emb/emb_amd64.s", true, "assembly beside a package's Go"},
|
||||
{"lib/lib_plan9.go", true, "a file one system builds"},
|
||||
{"plan9only/p.go", true, "what a file one system builds imports"},
|
||||
{"emb/static/index.html", true, "an embedded file"},
|
||||
{"emb/static/deep/style.css", true, "an embedded file below the pattern's directory"},
|
||||
{"emb/a b.txt", true, "an embed named outright, quoted"},
|
||||
{"vendor/example.org/dep/dep.go", true, "a vendored package it imports"},
|
||||
{"vendor/example.org/dep/sub/sub.go", true, "what a vendored package imports"},
|
||||
{"vendor/example.net/local/pkg/p.go", true, "a replaced module, as vendored"},
|
||||
{"third_party/local/pkg/p.go", true, "a replaced module, at its directory"},
|
||||
{"third_party/local/go.mod", true, "a replaced module's requirements"},
|
||||
{"go.mod", true, "the module's requirements"},
|
||||
{"go.sum", true, "the module's sums"},
|
||||
{"vendor/modules.txt", true, "the vendored modules"},
|
||||
{"go.work", true, "a workspace, were one made"},
|
||||
{"lib/lib_test.go", false, "a test is not built"},
|
||||
{"testonly/t.go", false, "a package only a test imports"},
|
||||
{"lib/sub/sub.go", false, "a package below an imported one, not imported"},
|
||||
{"unrelated/u.go", false, "a package nothing imports"},
|
||||
{"vendor/example.org/other/other.go", false, "a vendored package nothing imports"},
|
||||
{"README.md", false, "a file no build reads"},
|
||||
} {
|
||||
if SourceHolds(got, c.file) != c.held {
|
||||
t.Errorf("%s (%s): held %v, wanted %v\n %v", c.file, c.why, !c.held, c.held, got)
|
||||
}
|
||||
}
|
||||
|
||||
// **Never narrower than go list -deps**: every package go names, and every file it compiles or embeds,
|
||||
// is held. Where no go command is at hand, said, not passed.
|
||||
goCmd, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("NOT COMPARED: no go command to list the closure with")
|
||||
}
|
||||
list := exec.Command(goCmd, "list", "-deps", "-f",
|
||||
`{{if not .Standard}}{{$d := .Dir}}{{range .GoFiles}}{{$d}}/{{.}}
|
||||
{{end}}{{range .SFiles}}{{$d}}/{{.}}
|
||||
{{end}}{{range .EmbedFiles}}{{$d}}/{{.}}
|
||||
{{end}}{{end}}`, "./cmd/prog")
|
||||
list.Dir = root
|
||||
list.Env = append(os.Environ(), "GOFLAGS=-mod=vendor", "GOPROXY=off", "GOWORK=off", "GOOS=linux", "GOARCH=amd64")
|
||||
out, err := list.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("go list: %v\n%s", err, out)
|
||||
}
|
||||
real, _ := filepath.EvalSymlinks(root)
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
rel, err := filepath.Rel(real, line)
|
||||
if err != nil || strings.HasPrefix(rel, "..") {
|
||||
rel, _ = filepath.Rel(root, line)
|
||||
}
|
||||
if !SourceHolds(got, filepath.ToSlash(rel)) {
|
||||
t.Errorf("go list builds %s, and the build source does not hold it", rel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package builder
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -60,79 +59,6 @@ var aProgram = map[string]string{
|
||||
"README.md": "x",
|
||||
}
|
||||
|
||||
func TestAGoProgramsBuildSourceIsItsImportClosure(t *testing.T) {
|
||||
root := aGoTree(t, aProgram)
|
||||
got, err := GoBuildSource(root, "cmd/prog")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, c := range []struct {
|
||||
file string
|
||||
held bool
|
||||
why string
|
||||
}{
|
||||
{"cmd/prog/main.go", true, "the program itself"},
|
||||
{"cmd/prog/helper.go", true, "a file added to the program's package"},
|
||||
{"lib/lib.go", true, "a package it imports"},
|
||||
{"emb/emb_amd64.s", true, "assembly beside a package's Go"},
|
||||
{"lib/lib_plan9.go", true, "a file one system builds"},
|
||||
{"plan9only/p.go", true, "what a file one system builds imports"},
|
||||
{"emb/static/index.html", true, "an embedded file"},
|
||||
{"emb/static/deep/style.css", true, "an embedded file below the pattern's directory"},
|
||||
{"emb/a b.txt", true, "an embed named outright, quoted"},
|
||||
{"vendor/example.org/dep/dep.go", true, "a vendored package it imports"},
|
||||
{"vendor/example.org/dep/sub/sub.go", true, "what a vendored package imports"},
|
||||
{"vendor/example.net/local/pkg/p.go", true, "a replaced module, as vendored"},
|
||||
{"third_party/local/pkg/p.go", true, "a replaced module, at its directory"},
|
||||
{"third_party/local/go.mod", true, "a replaced module's requirements"},
|
||||
{"go.mod", true, "the module's requirements"},
|
||||
{"go.sum", true, "the module's sums"},
|
||||
{"vendor/modules.txt", true, "the vendored modules"},
|
||||
{"go.work", true, "a workspace, were one made"},
|
||||
{"lib/lib_test.go", false, "a test is not built"},
|
||||
{"testonly/t.go", false, "a package only a test imports"},
|
||||
{"lib/sub/sub.go", false, "a package below an imported one, not imported"},
|
||||
{"unrelated/u.go", false, "a package nothing imports"},
|
||||
{"vendor/example.org/other/other.go", false, "a vendored package nothing imports"},
|
||||
{"README.md", false, "a file no build reads"},
|
||||
} {
|
||||
if SourceHolds(got, c.file) != c.held {
|
||||
t.Errorf("%s (%s): held %v, wanted %v\n %v", c.file, c.why, !c.held, c.held, got)
|
||||
}
|
||||
}
|
||||
|
||||
// **Never narrower than go list -deps**: every package go names, and every file it compiles or embeds,
|
||||
// is held. Where no go command is at hand, said, not passed.
|
||||
goCmd, err := exec.LookPath("go")
|
||||
if err != nil {
|
||||
t.Skip("NOT COMPARED: no go command to list the closure with")
|
||||
}
|
||||
list := exec.Command(goCmd, "list", "-deps", "-f",
|
||||
`{{if not .Standard}}{{$d := .Dir}}{{range .GoFiles}}{{$d}}/{{.}}
|
||||
{{end}}{{range .SFiles}}{{$d}}/{{.}}
|
||||
{{end}}{{range .EmbedFiles}}{{$d}}/{{.}}
|
||||
{{end}}{{end}}`, "./cmd/prog")
|
||||
list.Dir = root
|
||||
list.Env = append(os.Environ(), "GOFLAGS=-mod=vendor", "GOPROXY=off", "GOWORK=off", "GOOS=linux", "GOARCH=amd64")
|
||||
out, err := list.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("go list: %v\n%s", err, out)
|
||||
}
|
||||
real, _ := filepath.EvalSymlinks(root)
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
rel, err := filepath.Rel(real, line)
|
||||
if err != nil || strings.HasPrefix(rel, "..") {
|
||||
rel, _ = filepath.Rel(root, line)
|
||||
}
|
||||
if !SourceHolds(got, filepath.ToSlash(rel)) {
|
||||
t.Errorf("go list builds %s, and the build source does not hold it", rel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A file added to the program's import closure moves its build source with it: the closure read again
|
||||
// grows by the package.
|
||||
func TestTheBuildSourceGrowsWithAnImport(t *testing.T) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAKilledBuildEndsItsWholeProcessGroup(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
child := filepath.Join(dir, "child")
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan error, 1)
|
||||
began := time.Now()
|
||||
go func() {
|
||||
// A shell that starts a grandchild and waits on it: killing the shell alone would leave the
|
||||
// grandchild running, holding the output open.
|
||||
_, err := Command(ctx, dir, "sh", "-c", `sleep 60 & echo $! > child; wait`)
|
||||
done <- err
|
||||
}()
|
||||
var pid int
|
||||
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
|
||||
if raw, err := os.ReadFile(child); err == nil && strings.TrimSpace(string(raw)) != "" {
|
||||
pid, _ = strconv.Atoi(strings.TrimSpace(string(raw)))
|
||||
break
|
||||
}
|
||||
}
|
||||
if pid == 0 {
|
||||
t.Fatal("the command never started its child")
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err == nil {
|
||||
t.Fatal("a killed command reported success")
|
||||
}
|
||||
case <-time.After(KillWait + 5*time.Second):
|
||||
t.Fatal("the killed command never returned")
|
||||
}
|
||||
if took := time.Since(began); took > KillWait {
|
||||
t.Errorf("ending the command took %s: the group was not killed, the wait ran out", took)
|
||||
}
|
||||
for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
|
||||
if err := syscall.Kill(pid, 0); errors.Is(err, syscall.ESRCH) {
|
||||
return
|
||||
}
|
||||
}
|
||||
_ = syscall.Kill(pid, syscall.SIGKILL)
|
||||
t.Fatalf("the grandchild %d outlived the kill", pid)
|
||||
}
|
||||
@@ -2,61 +2,13 @@ package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A build killed by hand (novox/hq ADR 0219) ends every command it started: the context's end kills
|
||||
// the command's whole process group, not the one process the context knows about.
|
||||
func TestAKilledBuildEndsItsWholeProcessGroup(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
child := filepath.Join(dir, "child")
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan error, 1)
|
||||
began := time.Now()
|
||||
go func() {
|
||||
// A shell that starts a grandchild and waits on it: killing the shell alone would leave the
|
||||
// grandchild running, holding the output open.
|
||||
_, err := Command(ctx, dir, "sh", "-c", `sleep 60 & echo $! > child; wait`)
|
||||
done <- err
|
||||
}()
|
||||
var pid int
|
||||
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
|
||||
if raw, err := os.ReadFile(child); err == nil && strings.TrimSpace(string(raw)) != "" {
|
||||
pid, _ = strconv.Atoi(strings.TrimSpace(string(raw)))
|
||||
break
|
||||
}
|
||||
}
|
||||
if pid == 0 {
|
||||
t.Fatal("the command never started its child")
|
||||
}
|
||||
cancel()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err == nil {
|
||||
t.Fatal("a killed command reported success")
|
||||
}
|
||||
case <-time.After(KillWait + 5*time.Second):
|
||||
t.Fatal("the killed command never returned")
|
||||
}
|
||||
if took := time.Since(began); took > KillWait {
|
||||
t.Errorf("ending the command took %s: the group was not killed, the wait ran out", took)
|
||||
}
|
||||
for deadline := time.Now().Add(2 * time.Second); time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
|
||||
if err := syscall.Kill(pid, 0); errors.Is(err, syscall.ESRCH) {
|
||||
return
|
||||
}
|
||||
}
|
||||
_ = syscall.Kill(pid, syscall.SIGKILL)
|
||||
t.Fatalf("the grandchild %d outlived the kill", pid)
|
||||
}
|
||||
|
||||
// Every `docker run` a build starts carries its id as a label; nothing else is touched.
|
||||
func TestEveryContainerABuildStartsCarriesItsID(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAnImageIsPushedOnlyAfterItsBasesAreLent(t *testing.T) {
|
||||
r, workspace := aRepository(t, anImage, map[string]string{"modules/bus/Dockerfile": "ARG BASE\nFROM ${BASE}"})
|
||||
r.contents["modules/bus/module.json"] = anImage
|
||||
l := &lending{mirroring: &mirroring{recorded: r, at: "127.0.0.1:5000"}}
|
||||
if _, err := Build(context.Background(), r.run, l, "https://forge.invalid/catalogue.git", "modules/bus", "", workspace,
|
||||
nil, Npmrc{}, GitCredential{}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{
|
||||
"lend bus/server 127.0.0.1:5000/upstream/docker.io/vendor/server@sha256:" + strings.Repeat("1", 64),
|
||||
"push bus/server",
|
||||
}
|
||||
if strings.Join(l.order, "\n") != strings.Join(want, "\n") {
|
||||
t.Fatalf("the build did:\n%s\nwant:\n%s", strings.Join(l.order, "\n"), strings.Join(want, "\n"))
|
||||
}
|
||||
}
|
||||
@@ -121,20 +121,3 @@ func (l *lending) PublishImage(ctx context.Context, localTag, repository string)
|
||||
l.order = append(l.order, "push "+repository)
|
||||
return l.mirroring.PublishImage(ctx, localTag, repository)
|
||||
}
|
||||
|
||||
func TestAnImageIsPushedOnlyAfterItsBasesAreLent(t *testing.T) {
|
||||
r, workspace := aRepository(t, anImage, map[string]string{"modules/bus/Dockerfile": "ARG BASE\nFROM ${BASE}"})
|
||||
r.contents["modules/bus/module.json"] = anImage
|
||||
l := &lending{mirroring: &mirroring{recorded: r, at: "127.0.0.1:5000"}}
|
||||
if _, err := Build(context.Background(), r.run, l, "https://forge.invalid/catalogue.git", "modules/bus", "", workspace,
|
||||
nil, Npmrc{}, GitCredential{}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{
|
||||
"lend bus/server 127.0.0.1:5000/upstream/docker.io/vendor/server@sha256:" + strings.Repeat("1", 64),
|
||||
"push bus/server",
|
||||
}
|
||||
if strings.Join(l.order, "\n") != strings.Join(want, "\n") {
|
||||
t.Fatalf("the build did:\n%s\nwant:\n%s", strings.Join(l.order, "\n"), strings.Join(want, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestABuildSaysWhatItMirroredEvenWhenItFails(t *testing.T) {
|
||||
// A recipe that reaches for an image nobody declared is refused — after the base was copied.
|
||||
r, workspace := aRepository(t, anImage, map[string]string{
|
||||
"modules/bus/Dockerfile": "ARG BASE\nFROM ${BASE}\nCOPY --from=vendor/other:1 /x /x"})
|
||||
r.contents["modules/bus/module.json"] = anImage
|
||||
r.tree = "aaaa"
|
||||
m := &mirroring{recorded: r, at: "registry-a:5000"}
|
||||
got, err := Build(context.Background(), r.run, m, "https://forge.invalid/catalogue.git", "modules/bus", "", workspace,
|
||||
nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a recipe fetching an undeclared image was built")
|
||||
}
|
||||
want := "registry-a:5000/upstream/docker.io/vendor/server@sha256:" + strings.Repeat("1", 64)
|
||||
if len(got.Mirrored) != 1 || got.Mirrored[0] != want {
|
||||
t.Fatalf("a failed build said it mirrored %v, want [%s]", got.Mirrored, want)
|
||||
}
|
||||
|
||||
// And a build that works says it too.
|
||||
r2, workspace2 := aRepository(t, anImage, map[string]string{"modules/bus/Dockerfile": "ARG BASE\nFROM ${BASE}"})
|
||||
r2.contents["modules/bus/module.json"] = anImage
|
||||
r2.tree = "aaaa"
|
||||
ok, err := Build(context.Background(), r2.run, &mirroring{recorded: r2, at: "registry-a:5000"},
|
||||
"https://forge.invalid/catalogue.git", "modules/bus", "", workspace2, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(ok.Mirrored) != 1 || ok.Mirrored[0] != want {
|
||||
t.Fatalf("a build said it mirrored %v, want [%s]", ok.Mirrored, want)
|
||||
}
|
||||
}
|
||||
@@ -219,36 +219,6 @@ func countPrefix(m map[string][]byte, prefix string) int {
|
||||
|
||||
// What a build copied is said whether it worked or not: the copy is in the store either way, and a
|
||||
// build that failed after copying is the one record that it is there.
|
||||
func TestABuildSaysWhatItMirroredEvenWhenItFails(t *testing.T) {
|
||||
// A recipe that reaches for an image nobody declared is refused — after the base was copied.
|
||||
r, workspace := aRepository(t, anImage, map[string]string{
|
||||
"modules/bus/Dockerfile": "ARG BASE\nFROM ${BASE}\nCOPY --from=vendor/other:1 /x /x"})
|
||||
r.contents["modules/bus/module.json"] = anImage
|
||||
r.tree = "aaaa"
|
||||
m := &mirroring{recorded: r, at: "registry-a:5000"}
|
||||
got, err := Build(context.Background(), r.run, m, "https://forge.invalid/catalogue.git", "modules/bus", "", workspace,
|
||||
nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a recipe fetching an undeclared image was built")
|
||||
}
|
||||
want := "registry-a:5000/upstream/docker.io/vendor/server@sha256:" + strings.Repeat("1", 64)
|
||||
if len(got.Mirrored) != 1 || got.Mirrored[0] != want {
|
||||
t.Fatalf("a failed build said it mirrored %v, want [%s]", got.Mirrored, want)
|
||||
}
|
||||
|
||||
// And a build that works says it too.
|
||||
r2, workspace2 := aRepository(t, anImage, map[string]string{"modules/bus/Dockerfile": "ARG BASE\nFROM ${BASE}"})
|
||||
r2.contents["modules/bus/module.json"] = anImage
|
||||
r2.tree = "aaaa"
|
||||
ok, err := Build(context.Background(), r2.run, &mirroring{recorded: r2, at: "registry-a:5000"},
|
||||
"https://forge.invalid/catalogue.git", "modules/bus", "", workspace2, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(ok.Mirrored) != 1 || ok.Mirrored[0] != want {
|
||||
t.Fatalf("a build said it mirrored %v, want [%s]", ok.Mirrored, want)
|
||||
}
|
||||
}
|
||||
|
||||
// held puts one image in a repository the old way and answers its index digest and the registry.
|
||||
func heldUnderAModule(t *testing.T) (*aRegistryOfRepositories, Registry, string, string, map[string][]byte, func()) {
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAnImageBuildGetsTheCredentialInTheContextAndHostNetwork(t *testing.T) {
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch\nCOPY .npmrc ./", "files/x": "y"})
|
||||
n := Npmrc{Scope: "@novox", Registry: "https://forge.invalid/api/packages/novox/npm/", Token: "t"}
|
||||
npmrc := filepath.Join(workspace, "source", ".npmrc")
|
||||
inContext := false
|
||||
run := func(ctx context.Context, dir, name string, args ...string) (string, error) {
|
||||
if name == "docker" && len(args) > 0 && args[0] == "build" {
|
||||
_, err := os.Stat(npmrc)
|
||||
inContext = err == nil
|
||||
}
|
||||
return r.run(ctx, dir, name, args...)
|
||||
}
|
||||
if _, err := Build(context.Background(), run, r,
|
||||
"https://forge.invalid/meshboard.git", "", "", workspace, nil, n, GitCredential{}, nil); err != nil {
|
||||
t.Fatalf("the build failed: %v", err)
|
||||
}
|
||||
|
||||
var build string
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker build") {
|
||||
build = line
|
||||
}
|
||||
}
|
||||
if build == "" {
|
||||
t.Fatal("no docker build ran")
|
||||
}
|
||||
if strings.Contains(build, "--secret") {
|
||||
t.Fatalf("the build used a buildkit secret, which this path avoids: %s", build)
|
||||
}
|
||||
if !strings.Contains(build, "--network host") {
|
||||
t.Fatalf("the build was not given the host network to reach the registry: %s", build)
|
||||
}
|
||||
// The .npmrc is written into the build context (the source tree), where a Dockerfile COPYs it, and
|
||||
// removed with the tree when the build ends: a later check's container mounts the workspace (novox/hq
|
||||
// issue 462).
|
||||
if !inContext {
|
||||
t.Fatal("the credential was not in the build context when the image was built")
|
||||
}
|
||||
if _, err := os.Stat(npmrc); !os.IsNotExist(err) {
|
||||
t.Fatalf("the credential outlives the build in the workspace: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnImageBuildWithoutACredentialGetsNoHostNetwork(t *testing.T) {
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch", "files/x": "y"})
|
||||
if _, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/meshboard.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil); err != nil {
|
||||
t.Fatalf("the build failed: %v", err)
|
||||
}
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker build") && (strings.Contains(line, "--secret") || strings.Contains(line, "--network host")) {
|
||||
t.Fatalf("a build with no credential was still given build-network or a secret: %s", line)
|
||||
}
|
||||
}
|
||||
tree := filepath.Join(workspace, "source")
|
||||
if _, err := os.Stat(filepath.Join(tree, ".npmrc")); err == nil {
|
||||
t.Fatal("an .npmrc was written into a build that has no credential")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPackageIsBuiltOnAPublicBaseAndPublishedByVersion(t *testing.T) {
|
||||
r, workspace := aRepository(t, aPackage, map[string]string{
|
||||
"package.json": `{"name":"@novox/mesh-sdk","version":"0.1.0"}`,
|
||||
})
|
||||
n := Npmrc{Scope: "@novox", Registry: "https://forge.invalid/api/packages/novox/npm/", Token: "t"}
|
||||
got, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/mesh-sdk.git", "", "", workspace, nil, n, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("the package did not build: %v", err)
|
||||
}
|
||||
if len(got.Built) != 1 || got.Built[0].Reference != "@novox/mesh-sdk@0.1.0" {
|
||||
t.Fatalf("a package is published by name and version, got %+v", got.Built)
|
||||
}
|
||||
if len(r.images) != 0 || len(r.archives) != 0 {
|
||||
t.Fatal("a package was pushed to the artifact store, which is not where packages live")
|
||||
}
|
||||
var ran string
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker run") {
|
||||
ran = line
|
||||
}
|
||||
}
|
||||
if ran == "" {
|
||||
t.Fatal("nothing ran to build the package")
|
||||
}
|
||||
if !strings.Contains(ran, "node:22-bookworm-slim") {
|
||||
t.Fatalf("a package was not built on a public base: %s", ran)
|
||||
}
|
||||
if !strings.Contains(ran, ":/root/.npmrc:ro") {
|
||||
t.Fatalf("the credential was not mounted read-only for the publish: %s", ran)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPackageWithNoRegistryIsRefused(t *testing.T) {
|
||||
r, workspace := aRepository(t, aPackage, map[string]string{
|
||||
"package.json": `{"name":"@novox/mesh-sdk","version":"0.1.0"}`,
|
||||
})
|
||||
_, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/mesh-sdk.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a package built with no registry to publish to, silently")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnImageThatDoesNotAskForTheCredentialDoesNotGetIt(t *testing.T) {
|
||||
// A Dockerfile with no .npmrc reference (like the control plane's) must build clean: no .npmrc
|
||||
// in its context, no host network — so its image stays deterministic and the credential does not
|
||||
// leak into a build that never resolves a mesh package.
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch\nCOPY . .", "files/x": "y"})
|
||||
n := Npmrc{Scope: "@novox", Registry: "https://forge.invalid/api/packages/novox/npm/", Token: "t"}
|
||||
if _, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/meshboard.git", "", "", workspace, nil, n, GitCredential{}, nil); err != nil {
|
||||
t.Fatalf("the build failed: %v", err)
|
||||
}
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker build") && strings.Contains(line, "--network host") {
|
||||
t.Fatalf("a build that does not ask for the credential got the host network: %s", line)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(workspace, "source", ".npmrc")); err == nil {
|
||||
t.Fatal("an .npmrc was written into a build that does not reference it")
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -67,65 +64,6 @@ func TestNpmrcDisabledUntilThereIsARegistry(t *testing.T) {
|
||||
// COPY in a stage it does not publish — and the build runs on the host network so a RUN resolving the
|
||||
// registry reaches it where the binding says (novox/hq ADR 0076). Not a buildkit secret, because this
|
||||
// machine's docker may carry no buildx.
|
||||
func TestAnImageBuildGetsTheCredentialInTheContextAndHostNetwork(t *testing.T) {
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch\nCOPY .npmrc ./", "files/x": "y"})
|
||||
n := Npmrc{Scope: "@novox", Registry: "https://forge.invalid/api/packages/novox/npm/", Token: "t"}
|
||||
npmrc := filepath.Join(workspace, "source", ".npmrc")
|
||||
inContext := false
|
||||
run := func(ctx context.Context, dir, name string, args ...string) (string, error) {
|
||||
if name == "docker" && len(args) > 0 && args[0] == "build" {
|
||||
_, err := os.Stat(npmrc)
|
||||
inContext = err == nil
|
||||
}
|
||||
return r.run(ctx, dir, name, args...)
|
||||
}
|
||||
if _, err := Build(context.Background(), run, r,
|
||||
"https://forge.invalid/meshboard.git", "", "", workspace, nil, n, GitCredential{}, nil); err != nil {
|
||||
t.Fatalf("the build failed: %v", err)
|
||||
}
|
||||
|
||||
var build string
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker build") {
|
||||
build = line
|
||||
}
|
||||
}
|
||||
if build == "" {
|
||||
t.Fatal("no docker build ran")
|
||||
}
|
||||
if strings.Contains(build, "--secret") {
|
||||
t.Fatalf("the build used a buildkit secret, which this path avoids: %s", build)
|
||||
}
|
||||
if !strings.Contains(build, "--network host") {
|
||||
t.Fatalf("the build was not given the host network to reach the registry: %s", build)
|
||||
}
|
||||
// The .npmrc is written into the build context (the source tree), where a Dockerfile COPYs it, and
|
||||
// removed with the tree when the build ends: a later check's container mounts the workspace (novox/hq
|
||||
// issue 462).
|
||||
if !inContext {
|
||||
t.Fatal("the credential was not in the build context when the image was built")
|
||||
}
|
||||
if _, err := os.Stat(npmrc); !os.IsNotExist(err) {
|
||||
t.Fatalf("the credential outlives the build in the workspace: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnImageBuildWithoutACredentialGetsNoHostNetwork(t *testing.T) {
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch", "files/x": "y"})
|
||||
if _, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/meshboard.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil); err != nil {
|
||||
t.Fatalf("the build failed: %v", err)
|
||||
}
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker build") && (strings.Contains(line, "--secret") || strings.Contains(line, "--network host")) {
|
||||
t.Fatalf("a build with no credential was still given build-network or a secret: %s", line)
|
||||
}
|
||||
}
|
||||
tree := filepath.Join(workspace, "source")
|
||||
if _, err := os.Stat(filepath.Join(tree, ".npmrc")); err == nil {
|
||||
t.Fatal("an .npmrc was written into a build that has no credential")
|
||||
}
|
||||
}
|
||||
|
||||
const aPackage = `{"module":"mesh-sdk","version":"1",
|
||||
"build":{"artifacts":[{"name":"lib","kind":"package","language":"typescript"}]},
|
||||
@@ -133,49 +71,6 @@ const aPackage = `{"module":"mesh-sdk","version":"1",
|
||||
|
||||
// A package is compiled on a public base and published to the mesh's package registry by version,
|
||||
// with nothing pushed to the artifact store and the credential mounted, not baked (novox/hq ADR 0076).
|
||||
func TestAPackageIsBuiltOnAPublicBaseAndPublishedByVersion(t *testing.T) {
|
||||
r, workspace := aRepository(t, aPackage, map[string]string{
|
||||
"package.json": `{"name":"@novox/mesh-sdk","version":"0.1.0"}`,
|
||||
})
|
||||
n := Npmrc{Scope: "@novox", Registry: "https://forge.invalid/api/packages/novox/npm/", Token: "t"}
|
||||
got, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/mesh-sdk.git", "", "", workspace, nil, n, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("the package did not build: %v", err)
|
||||
}
|
||||
if len(got.Built) != 1 || got.Built[0].Reference != "@novox/mesh-sdk@0.1.0" {
|
||||
t.Fatalf("a package is published by name and version, got %+v", got.Built)
|
||||
}
|
||||
if len(r.images) != 0 || len(r.archives) != 0 {
|
||||
t.Fatal("a package was pushed to the artifact store, which is not where packages live")
|
||||
}
|
||||
var ran string
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker run") {
|
||||
ran = line
|
||||
}
|
||||
}
|
||||
if ran == "" {
|
||||
t.Fatal("nothing ran to build the package")
|
||||
}
|
||||
if !strings.Contains(ran, "node:22-bookworm-slim") {
|
||||
t.Fatalf("a package was not built on a public base: %s", ran)
|
||||
}
|
||||
if !strings.Contains(ran, ":/root/.npmrc:ro") {
|
||||
t.Fatalf("the credential was not mounted read-only for the publish: %s", ran)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPackageWithNoRegistryIsRefused(t *testing.T) {
|
||||
r, workspace := aRepository(t, aPackage, map[string]string{
|
||||
"package.json": `{"name":"@novox/mesh-sdk","version":"0.1.0"}`,
|
||||
})
|
||||
_, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/mesh-sdk.git", "", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a package built with no registry to publish to, silently")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNpmrcRendersBasicAuthWhenGivenAUserAndPassword(t *testing.T) {
|
||||
n := Npmrc{
|
||||
@@ -210,23 +105,3 @@ func TestNpmrcRefusesWhenGivenNeitherTokenNorPassword(t *testing.T) {
|
||||
t.Fatal("a username with no password rendered an .npmrc")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnImageThatDoesNotAskForTheCredentialDoesNotGetIt(t *testing.T) {
|
||||
// A Dockerfile with no .npmrc reference (like the control plane's) must build clean: no .npmrc
|
||||
// in its context, no host network — so its image stays deterministic and the credential does not
|
||||
// leak into a build that never resolves a mesh package.
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch\nCOPY . .", "files/x": "y"})
|
||||
n := Npmrc{Scope: "@novox", Registry: "https://forge.invalid/api/packages/novox/npm/", Token: "t"}
|
||||
if _, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/meshboard.git", "", "", workspace, nil, n, GitCredential{}, nil); err != nil {
|
||||
t.Fatalf("the build failed: %v", err)
|
||||
}
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker build") && strings.Contains(line, "--network host") {
|
||||
t.Fatalf("a build that does not ask for the credential got the host network: %s", line)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(workspace, "source", ".npmrc")); err == nil {
|
||||
t.Fatal("an .npmrc was written into a build that does not reference it")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestABuildNeedingTheRegistryIsRefusedOnAnAgentWithoutOne(t *testing.T) {
|
||||
none := Npmrc{Machine: "workstation"}
|
||||
cases := map[string]struct {
|
||||
manifest string
|
||||
files map[string]string
|
||||
held map[string]string
|
||||
}{
|
||||
"a package": {aPackage, map[string]string{"package.json": `{"name":"@novox/mesh-sdk","version":"0.1.0"}`}, nil},
|
||||
"an image naming .npmrc": {withBoth, map[string]string{
|
||||
"Dockerfile": "FROM scratch\nCOPY .npmrc /root/.npmrc\n", "files/x": "y"}, nil},
|
||||
"a TypeScript bundle depending on the SDK": {aBundle, map[string]string{
|
||||
"index.ts": "console.log(1)", "package.json": `{"dependencies":{"@novox/mesh-sdk":"^0.1.0"}}`},
|
||||
map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}},
|
||||
}
|
||||
for name, c := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
r, workspace := aRepository(t, c.manifest, c.files)
|
||||
var said []string
|
||||
_, err := Build(context.Background(), compiling{r}.run, r,
|
||||
"https://forge.invalid/m.git", "", "", workspace, c.held, none, GitCredential{},
|
||||
func(step, message string) { said = append(said, step+": "+message) })
|
||||
if err == nil {
|
||||
t.Fatal("built with no package registry")
|
||||
}
|
||||
for _, want := range []string{"workstation", "npm-package-registry", "unbound", "gitea"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("the refusal does not say %q: %v", want, err)
|
||||
}
|
||||
}
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker build") || strings.HasPrefix(line, "docker run") ||
|
||||
strings.HasPrefix(line, "docker pull") {
|
||||
t.Errorf("something was built after the refusal: %s", line)
|
||||
}
|
||||
}
|
||||
if len(r.images) != 0 || len(r.archives) != 0 {
|
||||
t.Errorf("something was published after the refusal: %v %v", r.images, r.archives)
|
||||
}
|
||||
if !strings.Contains(strings.Join(said, "\n"), "REFUSED") {
|
||||
t.Errorf("the build's log does not say it was refused:\n%s", strings.Join(said, "\n"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// A Go artifact builds on the same agent.
|
||||
files := aSharedRepository("one", "1")
|
||||
manifest := `{"module":"ctl","version":"1","build":{"artifacts":[
|
||||
{"name":"controller","kind":"bundle","language":"go","system":"arch","from":"cmd/ctl","binary":"ctl"}]},
|
||||
"resources":[{"id":"controller","type":"process","name":"ctl","artifact":"controller","run":["./ctl"]}]}`
|
||||
r := &recorded{contents: map[string]string{ManifestName: manifest}}
|
||||
for k, v := range files {
|
||||
r.contents[k] = v
|
||||
}
|
||||
held := map[string]string{"mesh-tools-go/build": "registry.invalid/mesh-tools-go/build@sha256:" + strings.Repeat("b", 64)}
|
||||
if _, err := Build(context.Background(), onTrunk{recorded: r}.run, r,
|
||||
"https://forge.invalid/ctl.git", "", "", t.TempDir(), held, none, GitCredential{}, nil); err != nil {
|
||||
t.Fatalf("a Go artifact did not build on an agent without the registry: %v", err)
|
||||
}
|
||||
// And an image that does not name .npmrc.
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch\nCOPY . .", "files/x": "y"})
|
||||
if _, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/meshboard.git", "", "", workspace, nil, none, GitCredential{}, nil); err != nil {
|
||||
t.Fatalf("an image not naming .npmrc did not build on an agent without the registry: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABuildSaysWhichRegistryItResolvedFrom(t *testing.T) {
|
||||
for _, from := range []string{FromBinding, FromGenesis} {
|
||||
r, workspace := aRepository(t, aPackage, map[string]string{
|
||||
"package.json": `{"name":"@novox/mesh-sdk","version":"0.1.0"}`,
|
||||
})
|
||||
n := Npmrc{Scope: "@novox", Registry: "http://127.0.0.1:3000/api/packages/novox/npm/",
|
||||
Username: "mesh-builder", Password: "p", From: from, Machine: "anchor"}
|
||||
var said []string
|
||||
if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/mesh-sdk.git", "", "",
|
||||
workspace, nil, n, GitCredential{}, func(step, message string) { said = append(said, step+": "+message) }); err != nil {
|
||||
t.Fatalf("%s: %v", from, err)
|
||||
}
|
||||
if !strings.Contains(strings.Join(said, "\n"), "from "+from) {
|
||||
t.Errorf("the log does not say the registry came from %s:\n%s", from, strings.Join(said, "\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/beside"
|
||||
@@ -17,90 +15,8 @@ import (
|
||||
// with an `@novox/` dependency each fail before anything is built or published, naming the machine,
|
||||
// the unbound requirement and the module that provides it. A Go artifact, and an image that does not
|
||||
// name .npmrc, build there as before.
|
||||
func TestABuildNeedingTheRegistryIsRefusedOnAnAgentWithoutOne(t *testing.T) {
|
||||
none := Npmrc{Machine: "workstation"}
|
||||
cases := map[string]struct {
|
||||
manifest string
|
||||
files map[string]string
|
||||
held map[string]string
|
||||
}{
|
||||
"a package": {aPackage, map[string]string{"package.json": `{"name":"@novox/mesh-sdk","version":"0.1.0"}`}, nil},
|
||||
"an image naming .npmrc": {withBoth, map[string]string{
|
||||
"Dockerfile": "FROM scratch\nCOPY .npmrc /root/.npmrc\n", "files/x": "y"}, nil},
|
||||
"a TypeScript bundle depending on the SDK": {aBundle, map[string]string{
|
||||
"index.ts": "console.log(1)", "package.json": `{"dependencies":{"@novox/mesh-sdk":"^0.1.0"}}`},
|
||||
map[string]string{"mesh-tools/build": "registry.invalid/mesh-tools/build@sha256:" + strings.Repeat("b", 64)}},
|
||||
}
|
||||
for name, c := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
r, workspace := aRepository(t, c.manifest, c.files)
|
||||
var said []string
|
||||
_, err := Build(context.Background(), compiling{r}.run, r,
|
||||
"https://forge.invalid/m.git", "", "", workspace, c.held, none, GitCredential{},
|
||||
func(step, message string) { said = append(said, step+": "+message) })
|
||||
if err == nil {
|
||||
t.Fatal("built with no package registry")
|
||||
}
|
||||
for _, want := range []string{"workstation", "npm-package-registry", "unbound", "gitea"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("the refusal does not say %q: %v", want, err)
|
||||
}
|
||||
}
|
||||
for _, line := range r.ran {
|
||||
if strings.HasPrefix(line, "docker build") || strings.HasPrefix(line, "docker run") ||
|
||||
strings.HasPrefix(line, "docker pull") {
|
||||
t.Errorf("something was built after the refusal: %s", line)
|
||||
}
|
||||
}
|
||||
if len(r.images) != 0 || len(r.archives) != 0 {
|
||||
t.Errorf("something was published after the refusal: %v %v", r.images, r.archives)
|
||||
}
|
||||
if !strings.Contains(strings.Join(said, "\n"), "REFUSED") {
|
||||
t.Errorf("the build's log does not say it was refused:\n%s", strings.Join(said, "\n"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// A Go artifact builds on the same agent.
|
||||
files := aSharedRepository("one", "1")
|
||||
manifest := `{"module":"ctl","version":"1","build":{"artifacts":[
|
||||
{"name":"controller","kind":"bundle","language":"go","system":"arch","from":"cmd/ctl","binary":"ctl"}]},
|
||||
"resources":[{"id":"controller","type":"process","name":"ctl","artifact":"controller","run":["./ctl"]}]}`
|
||||
r := &recorded{contents: map[string]string{ManifestName: manifest}}
|
||||
for k, v := range files {
|
||||
r.contents[k] = v
|
||||
}
|
||||
held := map[string]string{"mesh-tools-go/build": "registry.invalid/mesh-tools-go/build@sha256:" + strings.Repeat("b", 64)}
|
||||
if _, err := Build(context.Background(), onTrunk{recorded: r}.run, r,
|
||||
"https://forge.invalid/ctl.git", "", "", t.TempDir(), held, none, GitCredential{}, nil); err != nil {
|
||||
t.Fatalf("a Go artifact did not build on an agent without the registry: %v", err)
|
||||
}
|
||||
// And an image that does not name .npmrc.
|
||||
r, workspace := aRepository(t, withBoth, map[string]string{"Dockerfile": "FROM scratch\nCOPY . .", "files/x": "y"})
|
||||
if _, err := Build(context.Background(), r.run, r,
|
||||
"https://forge.invalid/meshboard.git", "", "", workspace, nil, none, GitCredential{}, nil); err != nil {
|
||||
t.Fatalf("an image not naming .npmrc did not build on an agent without the registry: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// **The build's log says which registry it resolved from** (novox/hq ADR 0336 §4).
|
||||
func TestABuildSaysWhichRegistryItResolvedFrom(t *testing.T) {
|
||||
for _, from := range []string{FromBinding, FromGenesis} {
|
||||
r, workspace := aRepository(t, aPackage, map[string]string{
|
||||
"package.json": `{"name":"@novox/mesh-sdk","version":"0.1.0"}`,
|
||||
})
|
||||
n := Npmrc{Scope: "@novox", Registry: "http://127.0.0.1:3000/api/packages/novox/npm/",
|
||||
Username: "mesh-builder", Password: "p", From: from, Machine: "anchor"}
|
||||
var said []string
|
||||
if _, err := Build(context.Background(), r.run, r, "https://forge.invalid/mesh-sdk.git", "", "",
|
||||
workspace, nil, n, GitCredential{}, func(step, message string) { said = append(said, step+": "+message) }); err != nil {
|
||||
t.Fatalf("%s: %v", from, err)
|
||||
}
|
||||
if !strings.Contains(strings.Join(said, "\n"), "from "+from) {
|
||||
t.Errorf("the log does not say the registry came from %s:\n%s", from, strings.Join(said, "\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The refusal names the module that provides the registry; it is the catalogue's provider.
|
||||
func TestTheRegistryABuildIsRefusedForNamesItsProvider(t *testing.T) {
|
||||
|
||||
@@ -76,13 +76,13 @@ func TestABuildWaitsForARegistryHeldStillAndGoesOn(t *testing.T) {
|
||||
waitingFor(t, 5*time.Second)
|
||||
said := saying(t)
|
||||
f := &fakeRegistry{}
|
||||
r := Registry{Address: heldStill(t, f, 400*time.Millisecond)}
|
||||
r := Registry{Address: heldStill(t, f, 100*time.Millisecond)}
|
||||
body := []byte("a theme")
|
||||
sum := sha256.Sum256(body)
|
||||
digest := "sha256:" + hex.EncodeToString(sum[:])
|
||||
|
||||
if _, err := r.PublishArchive(context.Background(), "shell/config", body, digest); err != nil {
|
||||
t.Fatalf("a registry refusing for 400ms failed the build: %v", err)
|
||||
t.Fatalf("a registry refusing for 100ms failed the build: %v", err)
|
||||
}
|
||||
if string(f.blobs[digest]) != "a theme" {
|
||||
t.Fatalf("the registry holds %q", f.blobs[digest])
|
||||
@@ -97,7 +97,7 @@ func TestABuildWaitsForARegistryHeldStillAndGoesOn(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestABuildFailsLoudlyOnARegistryRefusingPastTheBound(t *testing.T) {
|
||||
waitingFor(t, 300*time.Millisecond)
|
||||
waitingFor(t, 100*time.Millisecond)
|
||||
said := saying(t)
|
||||
f := &fakeRegistry{}
|
||||
r := Registry{Address: heldStill(t, f, -1)}
|
||||
@@ -113,7 +113,7 @@ func TestABuildFailsLoudlyOnARegistryRefusingPastTheBound(t *testing.T) {
|
||||
if !strings.Contains(err.Error(), "refused every connection for") || !strings.Contains(err.Error(), "connection refused") {
|
||||
t.Errorf("the failure does not say it waited and was refused throughout: %v", err)
|
||||
}
|
||||
if waited := time.Since(started); waited < 300*time.Millisecond {
|
||||
if waited := time.Since(started); waited < 100*time.Millisecond {
|
||||
t.Errorf("failed after %s, before the bound", waited)
|
||||
}
|
||||
if log := strings.Join(said(), "\n"); !strings.Contains(log, "waits for the registry") {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAGoBundleBuiltTwiceFromOneBuildSourceIsOneDigest(t *testing.T) {
|
||||
for _, tool := range []string{"go", "git"} {
|
||||
if _, err := exec.LookPath(tool); err != nil {
|
||||
t.Skipf("%s is not on this machine: %v", tool, err)
|
||||
}
|
||||
}
|
||||
chain, err := ToolchainFor("go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root := t.TempDir()
|
||||
repo := filepath.Join(root, "origin")
|
||||
files := map[string]string{
|
||||
"go.mod": "module example.com/ctl\n\ngo 1.22\n",
|
||||
"cmd/ctl/main.go": "package main\n\nfunc main() { println(\"ctl\") }\n",
|
||||
"README.md": "one\n",
|
||||
}
|
||||
for name, body := range files {
|
||||
if err := os.MkdirAll(filepath.Dir(filepath.Join(repo, name)), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repo, name), []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
git := func(dir string, args ...string) string {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", append([]string{"-c", "user.name=t", "-c", "user.email=t@example.invalid",
|
||||
"-c", "commit.gpgsign=false", "-c", "init.defaultBranch=main"}, args...)...)
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out)
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
git(repo, "init", "-q")
|
||||
git(repo, "add", ".")
|
||||
git(repo, "commit", "-q", "-m", "one")
|
||||
first := git(repo, "rev-parse", "HEAD")
|
||||
if err := os.WriteFile(filepath.Join(repo, "README.md"), []byte("two\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
git(repo, "commit", "-q", "-am", "only the README")
|
||||
second := git(repo, "rev-parse", "HEAD")
|
||||
|
||||
build := func(commit, at string) string {
|
||||
t.Helper()
|
||||
clone := filepath.Join(root, at)
|
||||
git(root, "clone", "-q", repo, clone)
|
||||
git(clone, "checkout", "-q", commit)
|
||||
out := filepath.Join(clone, Out("server"), "ctl")
|
||||
line := append([]string(nil), chain.Compile...)
|
||||
line = append(line, "-ldflags", strings.Join(chain.LinkerFlags, " "), chain.OutputFlag, out, "./cmd/ctl")
|
||||
cmd := exec.Command(line[0], line[1:]...)
|
||||
cmd.Dir = clone
|
||||
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local", "GOPROXY=off", "GOWORK=off")
|
||||
if b, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("%s: %v\n%s", strings.Join(line, " "), err, b)
|
||||
}
|
||||
body, err := os.ReadFile(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Contains(body, []byte(commit)) {
|
||||
t.Errorf("the binary built at %s carries the commit it was built at", commit[:12])
|
||||
}
|
||||
sum := sha256.Sum256(body)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
a, b := build(first, "a"), build(second, "elsewhere/b")
|
||||
if a != b {
|
||||
t.Fatalf("one build source, two digests: %s at %s, %s at %s (compiled with %q)",
|
||||
a[:12], first[:12], b[:12], second[:12], strings.Join(chain.Compile, " "))
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,7 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -19,82 +15,6 @@ import (
|
||||
//
|
||||
// The toolchain's own compile line is run here, as the build seat runs it inside the toolchain image: in
|
||||
// two clones at two paths, of two commits that differ outside the program. One digest is wanted.
|
||||
func TestAGoBundleBuiltTwiceFromOneBuildSourceIsOneDigest(t *testing.T) {
|
||||
for _, tool := range []string{"go", "git"} {
|
||||
if _, err := exec.LookPath(tool); err != nil {
|
||||
t.Skipf("%s is not on this machine: %v", tool, err)
|
||||
}
|
||||
}
|
||||
chain, err := ToolchainFor("go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root := t.TempDir()
|
||||
repo := filepath.Join(root, "origin")
|
||||
files := map[string]string{
|
||||
"go.mod": "module example.com/ctl\n\ngo 1.22\n",
|
||||
"cmd/ctl/main.go": "package main\n\nfunc main() { println(\"ctl\") }\n",
|
||||
"README.md": "one\n",
|
||||
}
|
||||
for name, body := range files {
|
||||
if err := os.MkdirAll(filepath.Dir(filepath.Join(repo, name)), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repo, name), []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
git := func(dir string, args ...string) string {
|
||||
t.Helper()
|
||||
cmd := exec.Command("git", append([]string{"-c", "user.name=t", "-c", "user.email=t@example.invalid",
|
||||
"-c", "commit.gpgsign=false", "-c", "init.defaultBranch=main"}, args...)...)
|
||||
cmd.Dir = dir
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out)
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
git(repo, "init", "-q")
|
||||
git(repo, "add", ".")
|
||||
git(repo, "commit", "-q", "-m", "one")
|
||||
first := git(repo, "rev-parse", "HEAD")
|
||||
if err := os.WriteFile(filepath.Join(repo, "README.md"), []byte("two\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
git(repo, "commit", "-q", "-am", "only the README")
|
||||
second := git(repo, "rev-parse", "HEAD")
|
||||
|
||||
build := func(commit, at string) string {
|
||||
t.Helper()
|
||||
clone := filepath.Join(root, at)
|
||||
git(root, "clone", "-q", repo, clone)
|
||||
git(clone, "checkout", "-q", commit)
|
||||
out := filepath.Join(clone, Out("server"), "ctl")
|
||||
line := append([]string(nil), chain.Compile...)
|
||||
line = append(line, "-ldflags", strings.Join(chain.LinkerFlags, " "), chain.OutputFlag, out, "./cmd/ctl")
|
||||
cmd := exec.Command(line[0], line[1:]...)
|
||||
cmd.Dir = clone
|
||||
cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local", "GOPROXY=off", "GOWORK=off")
|
||||
if b, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("%s: %v\n%s", strings.Join(line, " "), err, b)
|
||||
}
|
||||
body, err := os.ReadFile(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Contains(body, []byte(commit)) {
|
||||
t.Errorf("the binary built at %s carries the commit it was built at", commit[:12])
|
||||
}
|
||||
sum := sha256.Sum256(body)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
a, b := build(first, "a"), build(second, "elsewhere/b")
|
||||
if a != b {
|
||||
t.Fatalf("one build source, two digests: %s at %s, %s at %s (compiled with %q)",
|
||||
a[:12], first[:12], b[:12], second[:12], strings.Join(chain.Compile, " "))
|
||||
}
|
||||
}
|
||||
|
||||
// The toolchain says so itself, so a reader of the compile line sees it without running a build.
|
||||
func TestTheGoToolchainStampsNoVersionControl(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func aCheckBuilt(t *testing.T, scratch *aScratch, tree string) CheckBuilt {
|
||||
t.Helper()
|
||||
r, run, workspace := aModuleAt(t, tree)
|
||||
return aCheckBuiltBy(t, scratch, r, run, workspace)
|
||||
}
|
||||
|
||||
func aCheckBuiltBy(t *testing.T, scratch *aScratch, r *recorded, run Runner, workspace string) CheckBuilt {
|
||||
t.Helper()
|
||||
spec := CheckSpec{ID: "build-1", Owner: aPull.Owner, Repo: aPull.Repo, Number: aPull.Number,
|
||||
Builds: []CheckBuildSpec{{Module: "meshboard", Repository: "https://forge.invalid/meshboard.git", Path: "mod",
|
||||
Ref: "main"}}}
|
||||
built := buildForTheMerge(t.Context(), run, spec, CheckBuilding{Publish: r, Scratch: scratch}, workspace, "", "",
|
||||
CheckVerdict{Head: "abcdefabcdef", Trunk: "123456123456"}, GitCredential{}, nil, logging(nil))
|
||||
if len(built) != 1 || built[0].NotBuilt != "" || built[0].Key == "" {
|
||||
t.Fatalf("the check did not build the module for its merge: %+v", built)
|
||||
}
|
||||
if !dockerBuilt(r) {
|
||||
t.Fatal("the check built no image")
|
||||
}
|
||||
for repository := range r.images {
|
||||
if !strings.HasPrefix(repository, ScratchPrefix) {
|
||||
t.Fatalf("the check published %s outside the scratch namespace", repository)
|
||||
}
|
||||
}
|
||||
for repository := range r.archives {
|
||||
if !strings.HasPrefix(repository, ScratchPrefix) {
|
||||
t.Fatalf("the check published %s outside the scratch namespace", repository)
|
||||
}
|
||||
}
|
||||
return built[0]
|
||||
}
|
||||
|
||||
func TestAWalkWhoseKeyIsItsChecksBuildsNothingAndRegistersTheChecksDigest(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
checked := aCheckBuilt(t, scratch, "tree-one")
|
||||
|
||||
r, run, workspace := aModuleAt(t, "tree-one")
|
||||
got, err := BuildReusing(t.Context(), run, r, Reuse{Scratch: scratch, Pulls: []PullRef{aPull}, Named: named(checked)},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if dockerBuilt(r) || len(r.images) > 0 || len(r.archives) > 0 {
|
||||
t.Fatalf("the walk built again: ran %v, published %v %v", r.ran, r.images, r.archives)
|
||||
}
|
||||
if got.Reused == nil || got.Why != "" {
|
||||
t.Fatalf("the walk did not say it reused the check's build: reused %+v, why %q", got.Reused, got.Why)
|
||||
}
|
||||
if got.ReuseKey != checked.Key {
|
||||
t.Fatalf("the walk's key %s is not the check's %s", got.ReuseKey, checked.Key)
|
||||
}
|
||||
if len(scratch.took) != 2 {
|
||||
t.Fatalf("the walk copied %v, not the check's two artifacts", scratch.took)
|
||||
}
|
||||
for _, made := range checked.Made {
|
||||
registered := strings.Replace(made.Reference, ScratchPrefix, "", 1)
|
||||
found := false
|
||||
for _, b := range got.Built {
|
||||
found = found || (b.Reference == registered && b.Digest == made.Digest)
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("the walk registers %+v, not the check's %s copied", got.Built, made.Reference)
|
||||
}
|
||||
}
|
||||
if image, _ := got.Manifest.Resources[0]["image"].(string); image != "registry.invalid/meshboard/server@sha256:"+strings.Repeat("a", 64) {
|
||||
t.Fatalf("the manifest pins %q, not the check's digest in the module's namespace", image)
|
||||
}
|
||||
words := got.Reused.Words()
|
||||
if !strings.HasPrefix(words, "reused from the check of novox/meshboard#7 at head abcdefabcdef (merged into trunk 123456123456)") ||
|
||||
!strings.Contains(words, "sha256:") {
|
||||
t.Fatalf("the walk says %q", words)
|
||||
}
|
||||
}
|
||||
|
||||
func TestARecordNoVerdictNamesIsRefusedAndBuilt(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
checked := aCheckBuilt(t, scratch, "tree-one")
|
||||
for name, leave := range map[string][]NamedBuild{
|
||||
"no verdict": nil,
|
||||
"another digest": func() []NamedBuild {
|
||||
n := named(checked)
|
||||
n[0].Digests["server"] = "sha256:" + strings.Repeat("e", 64)
|
||||
return n
|
||||
}(),
|
||||
"another check": func() []NamedBuild { n := named(checked); n[0].Check = "build-9"; return n }(),
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
scratch.took = nil
|
||||
r, run, workspace := aModuleAt(t, "tree-one")
|
||||
got, err := BuildReusing(t.Context(), run, r, Reuse{Scratch: scratch, Pulls: []PullRef{aPull}, Named: leave},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Reused != nil || len(scratch.took) > 0 || !dockerBuilt(r) {
|
||||
t.Fatalf("a record no verdict names was taken: %+v, copied %v", got.Reused, scratch.took)
|
||||
}
|
||||
if !strings.Contains(got.Why, "no check verdict of the walk's pull requests at the head the forge merged names it") ||
|
||||
!strings.Contains(got.Why, "at head abcdefabcdef") {
|
||||
t.Fatalf("the walk says %q", got.Why)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWalkWhoseKeyDiffersBuildsAndSaysWhy(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
checked := aCheckBuilt(t, scratch, "tree-one")
|
||||
|
||||
r, run, workspace := aModuleAt(t, "tree-two")
|
||||
got, err := BuildReusing(t.Context(), run, r, Reuse{Scratch: scratch, Pulls: []PullRef{aPull}, Named: named(checked)},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !dockerBuilt(r) || got.Reused != nil {
|
||||
t.Fatalf("a walk whose source differs did not build: reused %+v", got.Reused)
|
||||
}
|
||||
if !strings.Contains(got.Why, "the key differs") || !strings.Contains(got.Why, partTree) {
|
||||
t.Fatalf("the walk says %q, not which part of the key differs", got.Why)
|
||||
}
|
||||
|
||||
// And a builder of another version builds too, saying so.
|
||||
withVersion(t, "builder-2")
|
||||
r, run, workspace = aModuleAt(t, "tree-one")
|
||||
got, err = BuildReusing(t.Context(), run, r, Reuse{Scratch: scratch, Pulls: []PullRef{aPull}, Named: named(checked)},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !dockerBuilt(r) || !strings.Contains(got.Why, partBuilder) {
|
||||
t.Fatalf("a newer builder took the older one's build, or did not say why not: %q", got.Why)
|
||||
}
|
||||
|
||||
// No check of the walk's pull requests built it: said.
|
||||
r, run, workspace = aModuleAt(t, "tree-one")
|
||||
got, _ = BuildReusing(t.Context(), run, r, Reuse{Scratch: newScratch(), Pulls: []PullRef{aPull}},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if !dockerBuilt(r) || !strings.Contains(got.Why, "no check of novox/meshboard#7 built it") {
|
||||
t.Fatalf("the walk says %q", got.Why)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAFailedCopyOfAChecksBuildFallsBackToBuilding(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
checked := aCheckBuilt(t, scratch, "tree-one")
|
||||
scratch.failTake = true
|
||||
|
||||
r, run, workspace := aModuleAt(t, "tree-one")
|
||||
got, err := BuildReusing(t.Context(), run, r, Reuse{Scratch: scratch, Pulls: []PullRef{aPull}, Named: named(checked)},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("a failed copy failed the walk: %v", err)
|
||||
}
|
||||
if !dockerBuilt(r) || got.Reused != nil || len(r.images) == 0 {
|
||||
t.Fatal("a failed copy did not fall back to a build")
|
||||
}
|
||||
if !strings.Contains(got.Why, "copying meshboard's") || !strings.Contains(got.Why, "refused the mount") {
|
||||
t.Fatalf("the walk says %q", got.Why)
|
||||
}
|
||||
for repository := range r.images {
|
||||
if strings.HasPrefix(repository, ScratchPrefix) {
|
||||
t.Fatalf("the walk's own build went into the scratch namespace: %s", repository)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnlyAWalkOfATrunkCommitTakesAChecksBuild(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
checked := aCheckBuilt(t, scratch, "tree-one")
|
||||
|
||||
r, run, workspace := aModuleAt(t, "tree-one")
|
||||
if _, err := Build(t.Context(), run, r, "https://forge.invalid/meshboard.git", "mod", "main", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !dockerBuilt(r) || len(scratch.took) > 0 {
|
||||
t.Fatal("a build not asked to reuse took a check's build")
|
||||
}
|
||||
|
||||
r, _, workspace = aModuleAt(t, "tree-one") // no trunk said: off it
|
||||
got, err := BuildReusing(t.Context(), r.run, r, Reuse{Scratch: scratch, Named: named(checked)}, "https://forge.invalid/meshboard.git",
|
||||
"mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !dockerBuilt(r) || got.Reused != nil || !strings.Contains(got.Why, "not on its trunk") {
|
||||
t.Fatalf("a commit off the trunk took a check's build: %q", got.Why)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACheckThatFailedBuildsNothingForItsMerge(t *testing.T) {
|
||||
if skipsTheBuilds(CheckVerdict{Gate: &Layer{Verdict: "pass"}, Repo: &Layer{Verdict: "fail"}}) == "" {
|
||||
t.Fatal("a check whose repository layer failed builds for its merge")
|
||||
}
|
||||
if skipsTheBuilds(CheckVerdict{Gate: &Layer{Verdict: "pass"}, Repo: &Layer{Verdict: "warning"}}) != "" {
|
||||
t.Fatal("a check that passed with a warning builds nothing for its merge")
|
||||
}
|
||||
r, run, workspace := aModuleAt(t, "tree-one")
|
||||
if built := buildForTheMerge(t.Context(), run, CheckSpec{ID: "build-1"}, CheckBuilding{Publish: r, Scratch: newScratch()},
|
||||
workspace, "", "", CheckVerdict{}, GitCredential{}, nil, logging(nil)); built != nil || len(r.ran) > 0 {
|
||||
t.Fatalf("a check moving no module built %v, ran %v", built, r.ran)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACheckBuildsNoModuleWithoutAKeyNorWhatStandsAfterIt(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
r, run, workspace := aModuleAt(t, "tree-one")
|
||||
r.contents["mod/"+ManifestName] = `{"module":"meshboard","version":"1","build":{"artifacts":[
|
||||
{"name":"mirror","kind":"upstream","from":"docker.io/library/alpine:3"}]}}`
|
||||
spec := CheckSpec{ID: "build-1", Owner: "novox", Repo: "meshboard", Number: 7, Builds: []CheckBuildSpec{
|
||||
{Module: "meshboard", Repository: "https://forge.invalid/meshboard.git", Path: "mod", Tier: 0},
|
||||
{Module: "after", Repository: "https://forge.invalid/after.git", Tier: 1}}}
|
||||
built := buildForTheMerge(t.Context(), run, spec, CheckBuilding{Publish: r, Scratch: newScratch()}, workspace, "", "",
|
||||
CheckVerdict{}, GitCredential{}, nil, logging(nil))
|
||||
if len(built) != 2 || !strings.Contains(built[0].NotBuilt, "named by tag") ||
|
||||
!strings.Contains(built[1].NotBuilt, "stands after meshboard") {
|
||||
t.Fatalf("built %+v", built)
|
||||
}
|
||||
if len(r.images) > 0 {
|
||||
t.Fatalf("a module without a key was published: %v", r.images)
|
||||
}
|
||||
}
|
||||
|
||||
func aCheckAgain(t *testing.T, scratch *aScratch, tree string, leave []NamedBuild) (CheckBuilt, *recorded) {
|
||||
t.Helper()
|
||||
return aCheckOf(t, scratch, aPull, "abcdefabcdef", tree, leave)
|
||||
}
|
||||
|
||||
func aCheckOf(t *testing.T, scratch *aScratch, pull PullRef, head, tree string, leave []NamedBuild) (CheckBuilt, *recorded) {
|
||||
t.Helper()
|
||||
r, run, workspace := aModuleAt(t, tree)
|
||||
spec := CheckSpec{ID: "build-2", Owner: pull.Owner, Repo: pull.Repo, Number: pull.Number,
|
||||
Builds: []CheckBuildSpec{{Module: "meshboard", Repository: "https://forge.invalid/meshboard.git", Path: "mod",
|
||||
Ref: "main"}}}
|
||||
built := buildForTheMerge(t.Context(), run, spec, CheckBuilding{Publish: r, Scratch: scratch, Named: leave},
|
||||
workspace, "", "", CheckVerdict{Head: head, Trunk: "654321654321"}, GitCredential{}, nil, logging(nil))
|
||||
if len(built) != 1 || built[0].NotBuilt != "" || built[0].Key == "" {
|
||||
t.Fatalf("the second check did not answer the module for its merge: %+v", built)
|
||||
}
|
||||
return built[0], r
|
||||
}
|
||||
|
||||
func TestASecondCheckWhoseKeyIsUnchangedBuildsNothingAndNamesTheEarlierBuild(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
first := aCheckBuilt(t, scratch, "tree-one")
|
||||
|
||||
again, r := aCheckAgain(t, scratch, "tree-one", named(first))
|
||||
if dockerBuilt(r) || len(r.images) > 0 || len(r.archives) > 0 {
|
||||
t.Fatalf("the second check built again: ran %v, published %v %v", r.ran, r.images, r.archives)
|
||||
}
|
||||
if again.Key != first.Key || again.Kept != "build-1" || !reflect.DeepEqual(again.Made, first.Made) {
|
||||
t.Fatalf("the second check names %+v, not the first check's build %+v", again, first)
|
||||
}
|
||||
// The walk takes it by what the second verdict names.
|
||||
leave := named(again)
|
||||
leave[0].Check = again.Kept
|
||||
w, run, workspace := aModuleAt(t, "tree-one")
|
||||
got, err := BuildReusing(t.Context(), run, w, Reuse{Scratch: scratch, Pulls: []PullRef{aPull}, Named: leave},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil || got.Reused == nil || dockerBuilt(w) {
|
||||
t.Fatalf("the walk did not take the kept build: %v, why %q", err, got.Why)
|
||||
}
|
||||
}
|
||||
|
||||
func TestASecondCheckWhoseKeyChangedBuilds(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
first := aCheckBuilt(t, scratch, "tree-one")
|
||||
|
||||
again, r := aCheckAgain(t, scratch, "tree-two", named(first))
|
||||
if !dockerBuilt(r) || again.Kept != "" || again.Key == first.Key {
|
||||
t.Fatalf("a check whose key changed did not build: %+v", again)
|
||||
}
|
||||
}
|
||||
|
||||
func TestASecondCheckKeepsNoRecordAVerdictDoesNotName(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
first := aCheckBuilt(t, scratch, "tree-one")
|
||||
for name, leave := range map[string][]NamedBuild{
|
||||
"no verdict": nil,
|
||||
"another digest": func() []NamedBuild {
|
||||
n := named(first)
|
||||
n[0].Digests["server"] = "sha256:" + strings.Repeat("e", 64)
|
||||
return n
|
||||
}(),
|
||||
"another key": func() []NamedBuild { n := named(first); n[0].Key = "r1:" + strings.Repeat("f", 64); return n }(),
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
again, r := aCheckAgain(t, scratch, "tree-one", leave)
|
||||
if !dockerBuilt(r) || again.Kept != "" {
|
||||
t.Fatalf("a record no verdict names was kept: %+v", again)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACheckKeepsNoBuildOfAnotherHeadOrPullRequest(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
other := PullRef{Owner: "novox", Repo: "meshboard", Number: 8}
|
||||
for name, c := range map[string]struct {
|
||||
pull PullRef
|
||||
head string
|
||||
}{
|
||||
"another head": {aPull, "999999999999"},
|
||||
"another pull request": {other, "abcdefabcdef"},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
scratch := newScratch()
|
||||
first := aCheckBuilt(t, scratch, "tree-one")
|
||||
leave := named(first)
|
||||
leave[0].Pull = c.pull // as if its own verdict named it
|
||||
again, r := aCheckOf(t, scratch, c.pull, c.head, "tree-one", leave)
|
||||
if !dockerBuilt(r) || again.Kept != "" {
|
||||
t.Fatalf("a build of %s was kept: %+v", name, again)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -104,38 +103,8 @@ func dockerBuilt(r *recorded) bool {
|
||||
var aPull = PullRef{Owner: "novox", Repo: "meshboard", Number: 7}
|
||||
|
||||
// aCheckBuilt is the check of aPull building the module from a tree, into the scratch namespace.
|
||||
func aCheckBuilt(t *testing.T, scratch *aScratch, tree string) CheckBuilt {
|
||||
t.Helper()
|
||||
r, run, workspace := aModuleAt(t, tree)
|
||||
return aCheckBuiltBy(t, scratch, r, run, workspace)
|
||||
}
|
||||
|
||||
// aCheckBuiltBy is aCheckBuilt of the repository given.
|
||||
func aCheckBuiltBy(t *testing.T, scratch *aScratch, r *recorded, run Runner, workspace string) CheckBuilt {
|
||||
t.Helper()
|
||||
spec := CheckSpec{ID: "build-1", Owner: aPull.Owner, Repo: aPull.Repo, Number: aPull.Number,
|
||||
Builds: []CheckBuildSpec{{Module: "meshboard", Repository: "https://forge.invalid/meshboard.git", Path: "mod",
|
||||
Ref: "main"}}}
|
||||
built := buildForTheMerge(t.Context(), run, spec, CheckBuilding{Publish: r, Scratch: scratch}, workspace, "", "",
|
||||
CheckVerdict{Head: "abcdefabcdef", Trunk: "123456123456"}, GitCredential{}, nil, logging(nil))
|
||||
if len(built) != 1 || built[0].NotBuilt != "" || built[0].Key == "" {
|
||||
t.Fatalf("the check did not build the module for its merge: %+v", built)
|
||||
}
|
||||
if !dockerBuilt(r) {
|
||||
t.Fatal("the check built no image")
|
||||
}
|
||||
for repository := range r.images {
|
||||
if !strings.HasPrefix(repository, ScratchPrefix) {
|
||||
t.Fatalf("the check published %s outside the scratch namespace", repository)
|
||||
}
|
||||
}
|
||||
for repository := range r.archives {
|
||||
if !strings.HasPrefix(repository, ScratchPrefix) {
|
||||
t.Fatalf("the check published %s outside the scratch namespace", repository)
|
||||
}
|
||||
}
|
||||
return built[0]
|
||||
}
|
||||
|
||||
// named is the walk's leave naming what a check's verdict says it built, as the controller reads it.
|
||||
func named(c CheckBuilt) []NamedBuild {
|
||||
@@ -159,211 +128,21 @@ func withVersion(t *testing.T, v string) {
|
||||
|
||||
// A merge whose reuse key equals its check's builds nothing again: the walk copies the check's build by digest
|
||||
// and registers that digest.
|
||||
func TestAWalkWhoseKeyIsItsChecksBuildsNothingAndRegistersTheChecksDigest(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
checked := aCheckBuilt(t, scratch, "tree-one")
|
||||
|
||||
r, run, workspace := aModuleAt(t, "tree-one")
|
||||
got, err := BuildReusing(t.Context(), run, r, Reuse{Scratch: scratch, Pulls: []PullRef{aPull}, Named: named(checked)},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if dockerBuilt(r) || len(r.images) > 0 || len(r.archives) > 0 {
|
||||
t.Fatalf("the walk built again: ran %v, published %v %v", r.ran, r.images, r.archives)
|
||||
}
|
||||
if got.Reused == nil || got.Why != "" {
|
||||
t.Fatalf("the walk did not say it reused the check's build: reused %+v, why %q", got.Reused, got.Why)
|
||||
}
|
||||
if got.ReuseKey != checked.Key {
|
||||
t.Fatalf("the walk's key %s is not the check's %s", got.ReuseKey, checked.Key)
|
||||
}
|
||||
if len(scratch.took) != 2 {
|
||||
t.Fatalf("the walk copied %v, not the check's two artifacts", scratch.took)
|
||||
}
|
||||
for _, made := range checked.Made {
|
||||
registered := strings.Replace(made.Reference, ScratchPrefix, "", 1)
|
||||
found := false
|
||||
for _, b := range got.Built {
|
||||
found = found || (b.Reference == registered && b.Digest == made.Digest)
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("the walk registers %+v, not the check's %s copied", got.Built, made.Reference)
|
||||
}
|
||||
}
|
||||
if image, _ := got.Manifest.Resources[0]["image"].(string); image != "registry.invalid/meshboard/server@sha256:"+strings.Repeat("a", 64) {
|
||||
t.Fatalf("the manifest pins %q, not the check's digest in the module's namespace", image)
|
||||
}
|
||||
words := got.Reused.Words()
|
||||
if !strings.HasPrefix(words, "reused from the check of novox/meshboard#7 at head abcdefabcdef (merged into trunk 123456123456)") ||
|
||||
!strings.Contains(words, "sha256:") {
|
||||
t.Fatalf("the walk says %q", words)
|
||||
}
|
||||
}
|
||||
|
||||
// A record no verdict names is refused and built instead, and the walk says so: the store takes writes from anything
|
||||
// on its network, so a record planted under another merge's key is never registered.
|
||||
func TestARecordNoVerdictNamesIsRefusedAndBuilt(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
checked := aCheckBuilt(t, scratch, "tree-one")
|
||||
for name, leave := range map[string][]NamedBuild{
|
||||
"no verdict": nil,
|
||||
"another digest": func() []NamedBuild {
|
||||
n := named(checked)
|
||||
n[0].Digests["server"] = "sha256:" + strings.Repeat("e", 64)
|
||||
return n
|
||||
}(),
|
||||
"another check": func() []NamedBuild { n := named(checked); n[0].Check = "build-9"; return n }(),
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
scratch.took = nil
|
||||
r, run, workspace := aModuleAt(t, "tree-one")
|
||||
got, err := BuildReusing(t.Context(), run, r, Reuse{Scratch: scratch, Pulls: []PullRef{aPull}, Named: leave},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Reused != nil || len(scratch.took) > 0 || !dockerBuilt(r) {
|
||||
t.Fatalf("a record no verdict names was taken: %+v, copied %v", got.Reused, scratch.took)
|
||||
}
|
||||
if !strings.Contains(got.Why, "no check verdict of the walk's pull requests at the head the forge merged names it") ||
|
||||
!strings.Contains(got.Why, "at head abcdefabcdef") {
|
||||
t.Fatalf("the walk says %q", got.Why)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A differing key builds, and says which part differs.
|
||||
func TestAWalkWhoseKeyDiffersBuildsAndSaysWhy(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
checked := aCheckBuilt(t, scratch, "tree-one")
|
||||
|
||||
r, run, workspace := aModuleAt(t, "tree-two")
|
||||
got, err := BuildReusing(t.Context(), run, r, Reuse{Scratch: scratch, Pulls: []PullRef{aPull}, Named: named(checked)},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !dockerBuilt(r) || got.Reused != nil {
|
||||
t.Fatalf("a walk whose source differs did not build: reused %+v", got.Reused)
|
||||
}
|
||||
if !strings.Contains(got.Why, "the key differs") || !strings.Contains(got.Why, partTree) {
|
||||
t.Fatalf("the walk says %q, not which part of the key differs", got.Why)
|
||||
}
|
||||
|
||||
// And a builder of another version builds too, saying so.
|
||||
withVersion(t, "builder-2")
|
||||
r, run, workspace = aModuleAt(t, "tree-one")
|
||||
got, err = BuildReusing(t.Context(), run, r, Reuse{Scratch: scratch, Pulls: []PullRef{aPull}, Named: named(checked)},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !dockerBuilt(r) || !strings.Contains(got.Why, partBuilder) {
|
||||
t.Fatalf("a newer builder took the older one's build, or did not say why not: %q", got.Why)
|
||||
}
|
||||
|
||||
// No check of the walk's pull requests built it: said.
|
||||
r, run, workspace = aModuleAt(t, "tree-one")
|
||||
got, _ = BuildReusing(t.Context(), run, r, Reuse{Scratch: newScratch(), Pulls: []PullRef{aPull}},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if !dockerBuilt(r) || !strings.Contains(got.Why, "no check of novox/meshboard#7 built it") {
|
||||
t.Fatalf("the walk says %q", got.Why)
|
||||
}
|
||||
}
|
||||
|
||||
// A copy that fails is said and falls back to a build; it never fails the walk.
|
||||
func TestAFailedCopyOfAChecksBuildFallsBackToBuilding(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
checked := aCheckBuilt(t, scratch, "tree-one")
|
||||
scratch.failTake = true
|
||||
|
||||
r, run, workspace := aModuleAt(t, "tree-one")
|
||||
got, err := BuildReusing(t.Context(), run, r, Reuse{Scratch: scratch, Pulls: []PullRef{aPull}, Named: named(checked)},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("a failed copy failed the walk: %v", err)
|
||||
}
|
||||
if !dockerBuilt(r) || got.Reused != nil || len(r.images) == 0 {
|
||||
t.Fatal("a failed copy did not fall back to a build")
|
||||
}
|
||||
if !strings.Contains(got.Why, "copying meshboard's") || !strings.Contains(got.Why, "refused the mount") {
|
||||
t.Fatalf("the walk says %q", got.Why)
|
||||
}
|
||||
for repository := range r.images {
|
||||
if strings.HasPrefix(repository, ScratchPrefix) {
|
||||
t.Fatalf("the walk's own build went into the scratch namespace: %s", repository)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only a walk takes a check's build: a plain build — a check's, a dry run's, `build --ref`'s — builds, and a commit
|
||||
// off the trunk never takes one.
|
||||
func TestOnlyAWalkOfATrunkCommitTakesAChecksBuild(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
checked := aCheckBuilt(t, scratch, "tree-one")
|
||||
|
||||
r, run, workspace := aModuleAt(t, "tree-one")
|
||||
if _, err := Build(t.Context(), run, r, "https://forge.invalid/meshboard.git", "mod", "main", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !dockerBuilt(r) || len(scratch.took) > 0 {
|
||||
t.Fatal("a build not asked to reuse took a check's build")
|
||||
}
|
||||
|
||||
r, _, workspace = aModuleAt(t, "tree-one") // no trunk said: off it
|
||||
got, err := BuildReusing(t.Context(), r.run, r, Reuse{Scratch: scratch, Named: named(checked)}, "https://forge.invalid/meshboard.git",
|
||||
"mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !dockerBuilt(r) || got.Reused != nil || !strings.Contains(got.Why, "not on its trunk") {
|
||||
t.Fatalf("a commit off the trunk took a check's build: %q", got.Why)
|
||||
}
|
||||
}
|
||||
|
||||
// A check that did not pass builds nothing for its merge, and one asked to build nothing builds nothing.
|
||||
func TestACheckThatFailedBuildsNothingForItsMerge(t *testing.T) {
|
||||
if skipsTheBuilds(CheckVerdict{Gate: &Layer{Verdict: "pass"}, Repo: &Layer{Verdict: "fail"}}) == "" {
|
||||
t.Fatal("a check whose repository layer failed builds for its merge")
|
||||
}
|
||||
if skipsTheBuilds(CheckVerdict{Gate: &Layer{Verdict: "pass"}, Repo: &Layer{Verdict: "warning"}}) != "" {
|
||||
t.Fatal("a check that passed with a warning builds nothing for its merge")
|
||||
}
|
||||
r, run, workspace := aModuleAt(t, "tree-one")
|
||||
if built := buildForTheMerge(t.Context(), run, CheckSpec{ID: "build-1"}, CheckBuilding{Publish: r, Scratch: newScratch()},
|
||||
workspace, "", "", CheckVerdict{}, GitCredential{}, nil, logging(nil)); built != nil || len(r.ran) > 0 {
|
||||
t.Fatalf("a check moving no module built %v, ran %v", built, r.ran)
|
||||
}
|
||||
}
|
||||
|
||||
// A module whose source does not pin its build has no key and is not built by the check; what stands after it
|
||||
// is not built either.
|
||||
func TestACheckBuildsNoModuleWithoutAKeyNorWhatStandsAfterIt(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
r, run, workspace := aModuleAt(t, "tree-one")
|
||||
r.contents["mod/"+ManifestName] = `{"module":"meshboard","version":"1","build":{"artifacts":[
|
||||
{"name":"mirror","kind":"upstream","from":"docker.io/library/alpine:3"}]}}`
|
||||
spec := CheckSpec{ID: "build-1", Owner: "novox", Repo: "meshboard", Number: 7, Builds: []CheckBuildSpec{
|
||||
{Module: "meshboard", Repository: "https://forge.invalid/meshboard.git", Path: "mod", Tier: 0},
|
||||
{Module: "after", Repository: "https://forge.invalid/after.git", Tier: 1}}}
|
||||
built := buildForTheMerge(t.Context(), run, spec, CheckBuilding{Publish: r, Scratch: newScratch()}, workspace, "", "",
|
||||
CheckVerdict{}, GitCredential{}, nil, logging(nil))
|
||||
if len(built) != 2 || !strings.Contains(built[0].NotBuilt, "named by tag") ||
|
||||
!strings.Contains(built[1].NotBuilt, "stands after meshboard") {
|
||||
t.Fatalf("built %+v", built)
|
||||
}
|
||||
if len(r.images) > 0 {
|
||||
t.Fatalf("a module without a key was published: %v", r.images)
|
||||
}
|
||||
}
|
||||
|
||||
// What the key names: every input a fresh build could differ by.
|
||||
func TestTheReuseKeyNamesEveryInputOfTheBuild(t *testing.T) {
|
||||
@@ -642,86 +421,16 @@ func TestABuildTwoChecksShareGoesWithTheLastOfThem(t *testing.T) {
|
||||
|
||||
// aCheckAgain is a later check (build-2) of aPull building the module from a tree, with the builds the build seat's
|
||||
// verdicts name for it (novox/hq issue 703).
|
||||
func aCheckAgain(t *testing.T, scratch *aScratch, tree string, leave []NamedBuild) (CheckBuilt, *recorded) {
|
||||
t.Helper()
|
||||
return aCheckOf(t, scratch, aPull, "abcdefabcdef", tree, leave)
|
||||
}
|
||||
|
||||
// aCheckOf is a later check (build-2) of a pull request at a head, the trunk moved since build-1.
|
||||
func aCheckOf(t *testing.T, scratch *aScratch, pull PullRef, head, tree string, leave []NamedBuild) (CheckBuilt, *recorded) {
|
||||
t.Helper()
|
||||
r, run, workspace := aModuleAt(t, tree)
|
||||
spec := CheckSpec{ID: "build-2", Owner: pull.Owner, Repo: pull.Repo, Number: pull.Number,
|
||||
Builds: []CheckBuildSpec{{Module: "meshboard", Repository: "https://forge.invalid/meshboard.git", Path: "mod",
|
||||
Ref: "main"}}}
|
||||
built := buildForTheMerge(t.Context(), run, spec, CheckBuilding{Publish: r, Scratch: scratch, Named: leave},
|
||||
workspace, "", "", CheckVerdict{Head: head, Trunk: "654321654321"}, GitCredential{}, nil, logging(nil))
|
||||
if len(built) != 1 || built[0].NotBuilt != "" || built[0].Key == "" {
|
||||
t.Fatalf("the second check did not answer the module for its merge: %+v", built)
|
||||
}
|
||||
return built[0], r
|
||||
}
|
||||
|
||||
// A check asked again when only the trunk moved, whose key is unchanged, builds nothing: it keeps the build an
|
||||
// earlier verdict names and names it by key and digest, so the walk can take it (novox/hq issue 703).
|
||||
func TestASecondCheckWhoseKeyIsUnchangedBuildsNothingAndNamesTheEarlierBuild(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
first := aCheckBuilt(t, scratch, "tree-one")
|
||||
|
||||
again, r := aCheckAgain(t, scratch, "tree-one", named(first))
|
||||
if dockerBuilt(r) || len(r.images) > 0 || len(r.archives) > 0 {
|
||||
t.Fatalf("the second check built again: ran %v, published %v %v", r.ran, r.images, r.archives)
|
||||
}
|
||||
if again.Key != first.Key || again.Kept != "build-1" || !reflect.DeepEqual(again.Made, first.Made) {
|
||||
t.Fatalf("the second check names %+v, not the first check's build %+v", again, first)
|
||||
}
|
||||
// The walk takes it by what the second verdict names.
|
||||
leave := named(again)
|
||||
leave[0].Check = again.Kept
|
||||
w, run, workspace := aModuleAt(t, "tree-one")
|
||||
got, err := BuildReusing(t.Context(), run, w, Reuse{Scratch: scratch, Pulls: []PullRef{aPull}, Named: leave},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil || got.Reused == nil || dockerBuilt(w) {
|
||||
t.Fatalf("the walk did not take the kept build: %v, why %q", err, got.Why)
|
||||
}
|
||||
}
|
||||
|
||||
// A check whose key changed builds, as before.
|
||||
func TestASecondCheckWhoseKeyChangedBuilds(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
first := aCheckBuilt(t, scratch, "tree-one")
|
||||
|
||||
again, r := aCheckAgain(t, scratch, "tree-two", named(first))
|
||||
if !dockerBuilt(r) || again.Kept != "" || again.Key == first.Key {
|
||||
t.Fatalf("a check whose key changed did not build: %+v", again)
|
||||
}
|
||||
}
|
||||
|
||||
// A scratch record no verdict names is never kept: the store takes writes from anything on its network, so a
|
||||
// check builds rather than name a planted record (novox/hq issue 703, ADR 0350 §2).
|
||||
func TestASecondCheckKeepsNoRecordAVerdictDoesNotName(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
scratch := newScratch()
|
||||
first := aCheckBuilt(t, scratch, "tree-one")
|
||||
for name, leave := range map[string][]NamedBuild{
|
||||
"no verdict": nil,
|
||||
"another digest": func() []NamedBuild {
|
||||
n := named(first)
|
||||
n[0].Digests["server"] = "sha256:" + strings.Repeat("e", 64)
|
||||
return n
|
||||
}(),
|
||||
"another key": func() []NamedBuild { n := named(first); n[0].Key = "r1:" + strings.Repeat("f", 64); return n }(),
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
again, r := aCheckAgain(t, scratch, "tree-one", leave)
|
||||
if !dockerBuilt(r) || again.Kept != "" {
|
||||
t.Fatalf("a record no verdict names was kept: %+v", again)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A build a later check kept stays when the check that built it goes: its artifacts and its record, which the later
|
||||
// check's verdict names (novox/hq issue 703, ADR 0350 §5).
|
||||
@@ -767,25 +476,3 @@ func TestABuildALaterCheckKeptStaysWhenTheCheckThatBuiltItGoes(t *testing.T) {
|
||||
|
||||
// Only an earlier check of the same pull request at the same head is kept from: another head of it, or another pull
|
||||
// request whose verdict names the same key and digests, builds (novox/hq issue 703).
|
||||
func TestACheckKeepsNoBuildOfAnotherHeadOrPullRequest(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
other := PullRef{Owner: "novox", Repo: "meshboard", Number: 8}
|
||||
for name, c := range map[string]struct {
|
||||
pull PullRef
|
||||
head string
|
||||
}{
|
||||
"another head": {aPull, "999999999999"},
|
||||
"another pull request": {other, "abcdefabcdef"},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
scratch := newScratch()
|
||||
first := aCheckBuilt(t, scratch, "tree-one")
|
||||
leave := named(first)
|
||||
leave[0].Pull = c.pull // as if its own verdict named it
|
||||
again, r := aCheckOf(t, scratch, c.pull, c.head, "tree-one", leave)
|
||||
if !dockerBuilt(r) || again.Kept != "" {
|
||||
t.Fatalf("a build of %s was kept: %+v", name, again)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func fingerprintOf(t *testing.T, manifest, tree string, mirrored string) string {
|
||||
t.Helper()
|
||||
r, workspace := aRepository(t, manifest, map[string]string{"modules/bus/Dockerfile": "ARG BASE\nFROM ${BASE}"})
|
||||
r.contents["modules/bus/module.json"] = manifest
|
||||
r.tree = tree
|
||||
m := &mirroring{recorded: r, at: mirrored}
|
||||
got, err := Build(context.Background(), r.run, m, "https://forge.invalid/catalogue.git", "modules/bus", "", workspace,
|
||||
nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return got.Source
|
||||
}
|
||||
|
||||
func TestASourceFingerprintNamesWhatABuildWasMadeFrom(t *testing.T) {
|
||||
one := fingerprintOf(t, anImage, "aaaa", "registry-a:5000")
|
||||
if !strings.HasPrefix(one, sourcePrefix) {
|
||||
t.Fatalf("no fingerprint: %q", one)
|
||||
}
|
||||
// The same tree and base, the base copied to another registry address: one source.
|
||||
if again := fingerprintOf(t, anImage, "aaaa", "registry-b:5000"); again != one {
|
||||
t.Fatalf("one source has two fingerprints: %s %s", one, again)
|
||||
}
|
||||
// The module's tree changed: another source.
|
||||
if changed := fingerprintOf(t, anImage, "bbbb", "registry-a:5000"); changed == one {
|
||||
t.Fatal("a changed tree kept its fingerprint")
|
||||
}
|
||||
// The base moved: another source.
|
||||
moved := strings.Replace(anImage, "1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"2222222222222222222222222222222222222222222222222222222222222222", 1)
|
||||
if changed := fingerprintOf(t, moved, "aaaa", "registry-a:5000"); changed == one {
|
||||
t.Fatal("a moved base kept its fingerprint")
|
||||
}
|
||||
}
|
||||
@@ -14,20 +14,6 @@ const anImage = `{"module":"bus","version":"1",
|
||||
"artifacts":[{"name":"server","kind":"image","from":"Dockerfile"}]},
|
||||
"resources":[{"id":"svc","type":"container","name":"bus","artifact":"server"}]}`
|
||||
|
||||
func fingerprintOf(t *testing.T, manifest, tree string, mirrored string) string {
|
||||
t.Helper()
|
||||
r, workspace := aRepository(t, manifest, map[string]string{"modules/bus/Dockerfile": "ARG BASE\nFROM ${BASE}"})
|
||||
r.contents["modules/bus/module.json"] = manifest
|
||||
r.tree = tree
|
||||
m := &mirroring{recorded: r, at: mirrored}
|
||||
got, err := Build(context.Background(), r.run, m, "https://forge.invalid/catalogue.git", "modules/bus", "", workspace,
|
||||
nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return got.Source
|
||||
}
|
||||
|
||||
// mirroring is a store that copies a vendor's image into the mesh's registry, at an address a test says.
|
||||
type mirroring struct {
|
||||
*recorded
|
||||
@@ -39,27 +25,6 @@ func (m *mirroring) MirrorImage(_ context.Context, from, repository string) (str
|
||||
return m.at + "/" + repository + "@" + digest, nil
|
||||
}
|
||||
|
||||
func TestASourceFingerprintNamesWhatABuildWasMadeFrom(t *testing.T) {
|
||||
one := fingerprintOf(t, anImage, "aaaa", "registry-a:5000")
|
||||
if !strings.HasPrefix(one, sourcePrefix) {
|
||||
t.Fatalf("no fingerprint: %q", one)
|
||||
}
|
||||
// The same tree and base, the base copied to another registry address: one source.
|
||||
if again := fingerprintOf(t, anImage, "aaaa", "registry-b:5000"); again != one {
|
||||
t.Fatalf("one source has two fingerprints: %s %s", one, again)
|
||||
}
|
||||
// The module's tree changed: another source.
|
||||
if changed := fingerprintOf(t, anImage, "bbbb", "registry-a:5000"); changed == one {
|
||||
t.Fatal("a changed tree kept its fingerprint")
|
||||
}
|
||||
// The base moved: another source.
|
||||
moved := strings.Replace(anImage, "1111111111111111111111111111111111111111111111111111111111111111",
|
||||
"2222222222222222222222222222222222222222222222222222222222222222", 1)
|
||||
if changed := fingerprintOf(t, moved, "aaaa", "registry-a:5000"); changed == one {
|
||||
t.Fatal("a moved base kept its fingerprint")
|
||||
}
|
||||
}
|
||||
|
||||
func TestABuildTheRegistryDecidesHasNoFingerprint(t *testing.T) {
|
||||
s := newSourceInputs("app")
|
||||
s.tree = "aaaa"
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
func TestABuildReadsEachTemplateBackFromItsArchive(t *testing.T) {
|
||||
r, workspace := aRepository(t, templatedModule, templatedFiles)
|
||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if text := contentOf(t, got.Manifest, "conf"); text != templatedFiles["files/meshboard.conf"] {
|
||||
t.Fatalf("the template's text is %q, not the file's", text)
|
||||
}
|
||||
if text := contentOf(t, got.Manifest, "deeper"); text != templatedFiles["files/a/deeper.conf"] {
|
||||
t.Fatalf("the template's text is %q, not the file's", text)
|
||||
}
|
||||
if r.archives["meshboard/files"] == "" {
|
||||
t.Fatalf("the templates archive was not published: %v", r.archives)
|
||||
}
|
||||
if problem := catalogue.HeldTemplateProblem(got.Manifest); problem != "" {
|
||||
t.Fatal(problem)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABuildWhoseTemplateIsMissingFailsNamingIt(t *testing.T) {
|
||||
files := map[string]string{}
|
||||
for k, v := range templatedFiles {
|
||||
files[k] = v
|
||||
}
|
||||
delete(files, "files/a/deeper.conf")
|
||||
r, workspace := aRepository(t, templatedModule, files)
|
||||
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "files/a/deeper.conf") {
|
||||
t.Fatalf("a build whose template is missing did not fail naming it: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWalkTakingAChecksBuildReadsTheTemplatesBackToo(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
contents := map[string]string{"mod/" + ManifestName: templatedModule}
|
||||
for k, v := range templatedFiles {
|
||||
contents["mod/"+k] = v
|
||||
}
|
||||
scratch := newScratch()
|
||||
r, run, workspace := aModuleWith(t, "tree-one", contents)
|
||||
checked := aCheckBuiltBy(t, scratch, r, run, workspace)
|
||||
|
||||
r, run, workspace = aModuleWith(t, "tree-one", contents)
|
||||
got, err := BuildReusing(t.Context(), run, r, Reuse{Scratch: scratch, Pulls: []PullRef{aPull}, Named: named(checked)},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Reused == nil || len(r.archives) > 0 {
|
||||
t.Fatalf("the walk built again: reused %+v, why %q, published %v", got.Reused, got.Why, r.archives)
|
||||
}
|
||||
if text := contentOf(t, got.Manifest, "conf"); text != templatedFiles["files/meshboard.conf"] {
|
||||
t.Fatalf("the template's text is %q, not the file's", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABuildReadsEachTemplateBackFromTheTemplatesDirectoryItsManifestNames(t *testing.T) {
|
||||
files := map[string]string{"files/bin/meshboard-hook": "#!/bin/sh\n", "templates/meshboard.conf": "listen = 8080\n"}
|
||||
r, workspace := aRepository(t, unpackingModule, files)
|
||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if text := contentOf(t, got.Manifest, "conf"); text != files["templates/meshboard.conf"] {
|
||||
t.Fatalf("the template's text is %q, not the file's", text)
|
||||
}
|
||||
if got.Manifest.Templates != "" {
|
||||
t.Fatalf("the built manifest still names its templates directory: %q", got.Manifest.Templates)
|
||||
}
|
||||
// files/ packs exactly as it would with no templates beside it.
|
||||
alone, err := pack(writeTree(t, t.TempDir(), map[string]string{"bin/meshboard-hook": "#!/bin/sh\n"}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sum := sha256.Sum256(alone)
|
||||
packed := ""
|
||||
for _, b := range got.Built {
|
||||
if b.Name == "scripts" {
|
||||
packed = b.Digest
|
||||
}
|
||||
}
|
||||
if packed != "sha256:"+hex.EncodeToString(sum[:]) {
|
||||
t.Fatalf("files/ packed to %q, not to what it packs to alone", packed)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
package builder
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -48,66 +45,8 @@ func contentOf(t *testing.T, m catalogue.Manifest, id string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestABuildReadsEachTemplateBackFromItsArchive(t *testing.T) {
|
||||
r, workspace := aRepository(t, templatedModule, templatedFiles)
|
||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if text := contentOf(t, got.Manifest, "conf"); text != templatedFiles["files/meshboard.conf"] {
|
||||
t.Fatalf("the template's text is %q, not the file's", text)
|
||||
}
|
||||
if text := contentOf(t, got.Manifest, "deeper"); text != templatedFiles["files/a/deeper.conf"] {
|
||||
t.Fatalf("the template's text is %q, not the file's", text)
|
||||
}
|
||||
if r.archives["meshboard/files"] == "" {
|
||||
t.Fatalf("the templates archive was not published: %v", r.archives)
|
||||
}
|
||||
if problem := catalogue.HeldTemplateProblem(got.Manifest); problem != "" {
|
||||
t.Fatal(problem)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABuildWhoseTemplateIsMissingFailsNamingIt(t *testing.T) {
|
||||
files := map[string]string{}
|
||||
for k, v := range templatedFiles {
|
||||
files[k] = v
|
||||
}
|
||||
delete(files, "files/a/deeper.conf")
|
||||
r, workspace := aRepository(t, templatedModule, files)
|
||||
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), "files/a/deeper.conf") {
|
||||
t.Fatalf("a build whose template is missing did not fail naming it: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// A walk that takes its check's build copies the archive by digest and builds nothing: the templates are read back
|
||||
// from the tree packed again, which is the archive only when it packs to the digest taken.
|
||||
func TestAWalkTakingAChecksBuildReadsTheTemplatesBackToo(t *testing.T) {
|
||||
withVersion(t, "builder-1")
|
||||
contents := map[string]string{"mod/" + ManifestName: templatedModule}
|
||||
for k, v := range templatedFiles {
|
||||
contents["mod/"+k] = v
|
||||
}
|
||||
scratch := newScratch()
|
||||
r, run, workspace := aModuleWith(t, "tree-one", contents)
|
||||
checked := aCheckBuiltBy(t, scratch, r, run, workspace)
|
||||
|
||||
r, run, workspace = aModuleWith(t, "tree-one", contents)
|
||||
got, err := BuildReusing(t.Context(), run, r, Reuse{Scratch: scratch, Pulls: []PullRef{aPull}, Named: named(checked)},
|
||||
"https://forge.invalid/meshboard.git", "mod", "main", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Reused == nil || len(r.archives) > 0 {
|
||||
t.Fatalf("the walk built again: reused %+v, why %q, published %v", got.Reused, got.Why, r.archives)
|
||||
}
|
||||
if text := contentOf(t, got.Manifest, "conf"); text != templatedFiles["files/meshboard.conf"] {
|
||||
t.Fatalf("the template's text is %q, not the file's", text)
|
||||
}
|
||||
}
|
||||
|
||||
// The tree packed again must be the archive taken; text read from anything else is not the archive's.
|
||||
func TestTemplateTextsAreRefusedWhenTheTreeDoesNotPackToTheDigestTaken(t *testing.T) {
|
||||
@@ -148,34 +87,3 @@ const unpackingModule = `{"module":"meshboard","version":"1",
|
||||
"resources":[
|
||||
{"id":"scripts","type":"archive","path":"/usr/local/lib/meshboard","artifact":"scripts"},
|
||||
{"id":"conf","type":"file","path":"/etc/meshboard/meshboard.conf","template":"templates/meshboard.conf"}]}`
|
||||
|
||||
func TestABuildReadsEachTemplateBackFromTheTemplatesDirectoryItsManifestNames(t *testing.T) {
|
||||
files := map[string]string{"files/bin/meshboard-hook": "#!/bin/sh\n", "templates/meshboard.conf": "listen = 8080\n"}
|
||||
r, workspace := aRepository(t, unpackingModule, files)
|
||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/meshboard.git", "", "", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if text := contentOf(t, got.Manifest, "conf"); text != files["templates/meshboard.conf"] {
|
||||
t.Fatalf("the template's text is %q, not the file's", text)
|
||||
}
|
||||
if got.Manifest.Templates != "" {
|
||||
t.Fatalf("the built manifest still names its templates directory: %q", got.Manifest.Templates)
|
||||
}
|
||||
// files/ packs exactly as it would with no templates beside it.
|
||||
alone, err := pack(writeTree(t, t.TempDir(), map[string]string{"bin/meshboard-hook": "#!/bin/sh\n"}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sum := sha256.Sum256(alone)
|
||||
packed := ""
|
||||
for _, b := range got.Built {
|
||||
if b.Name == "scripts" {
|
||||
packed = b.Digest
|
||||
}
|
||||
}
|
||||
if packed != "sha256:"+hex.EncodeToString(sum[:]) {
|
||||
t.Fatalf("files/ packed to %q, not to what it packs to alone", packed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@ const maxUpstream = 512 << 20
|
||||
|
||||
// maxEntries is the most entries an upstream archive may hold: millions of empty ones would fill the build
|
||||
// seat's inodes and the node's. Betterbird's release holds 64.
|
||||
const maxEntries = 100_000
|
||||
// It is a variable so that a test can lower it instead of writing a hundred thousand headers.
|
||||
var maxEntries = 100_000
|
||||
|
||||
// noDowngrade refuses a redirect off https: the digest still guards the bytes, and a fetch in the clear says
|
||||
// to whoever is on the path what the build seat fetches.
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
func TestAnArchivePinnedByURLIsFetchedCheckedAndPackedWithoutItsOwnDirectory(t *testing.T) {
|
||||
body := release(t)
|
||||
v := &vendor{files: map[string][]byte{"/release.tar.gz": body}}
|
||||
base := serveVendor(t, v)
|
||||
r, workspace := aRepository(t, pinnedManifest(base, sumHex(body)), nil)
|
||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/catalogue.git", "", "", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
digest, _ := got.Manifest.Resources[0]["digest"].(string)
|
||||
if !strings.HasPrefix(digest, "sha256:") || r.archives["betterbird/archive"] != digest {
|
||||
t.Fatalf("the release was not published and pinned as the module's archive: %v, published %v",
|
||||
got.Manifest.Resources[0], r.archives)
|
||||
}
|
||||
|
||||
packed, err := upstreamArchive(context.Background(), workspace, catalogue.Artifact{Name: "archive", Kind: "archive",
|
||||
URL: base + "/release.tar.gz", SHA256: sumHex(body), Version: "153.4.0esr-bb10", Within: "betterbird"},
|
||||
func(string, string, ...any) {})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if "sha256:"+sumHex(packed) != digest {
|
||||
t.Fatalf("a second packing of one pin gave another digest: %s and %s", digest, sumHex(packed))
|
||||
}
|
||||
entries := entriesOf(t, packed)
|
||||
want := map[string]string{
|
||||
"application.ini": "[App]\nVersion=153.4.0\n -rw-r--r--",
|
||||
"betterbird": "#!/bin/sh\n -rwxr-xr-x",
|
||||
"defaults/pref/channel-prefs.js": "pref(\"app.update.channel\", \"default\");\n -rw-r--r--",
|
||||
}
|
||||
if len(entries) != len(want) {
|
||||
t.Fatalf("the archive holds %v, want %v", entries, want)
|
||||
}
|
||||
for name, w := range want {
|
||||
if entries[name] != w {
|
||||
t.Errorf("%s is %q in the archive, want %q", name, entries[name], w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUpstreamFileIsFetchedOnceAndKeptByItsDigest(t *testing.T) {
|
||||
// The vendor of an add-on serves only its newest version at one address: a pin that was fetched once
|
||||
// must still build after the address moved on.
|
||||
body := release(t)
|
||||
v := &vendor{files: map[string][]byte{"/release.tar.gz": body}}
|
||||
base := serveVendor(t, v)
|
||||
r, workspace := aRepository(t, pinnedManifest(base, sumHex(body)), nil)
|
||||
first, err := Build(context.Background(), r.run, r, "https://forge.invalid/catalogue.git", "", "", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v.files["/release.tar.gz"] = []byte("the vendor's next release")
|
||||
second, err := Build(context.Background(), r.run, r, "https://forge.invalid/catalogue.git", "", "", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("a pin fetched before failed to build once the address served something else: %v", err)
|
||||
}
|
||||
if v.asked != 1 {
|
||||
t.Fatalf("the vendor was asked %d times for one pin, want once", v.asked)
|
||||
}
|
||||
if first.Manifest.Resources[0]["digest"] != second.Manifest.Resources[0]["digest"] {
|
||||
t.Fatal("two builds of one pin gave two digests")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUpstreamFileWhoseDownloadDoesNotMatchItsPinFailsAndNamesBothDigests(t *testing.T) {
|
||||
pinned := release(t)
|
||||
served := gzipped(t, tarOf(t, []tarEntry{{name: "betterbird/betterbird", body: "not what was reviewed", mode: 0o755}}))
|
||||
v := &vendor{files: map[string][]byte{"/release.tar.gz": served}}
|
||||
base := serveVendor(t, v)
|
||||
r, workspace := aRepository(t, pinnedManifest(base, sumHex(pinned)), nil)
|
||||
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/catalogue.git", "", "", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a download that is not the pinned file was built")
|
||||
}
|
||||
for _, digest := range []string{sumHex(pinned), sumHex(served)} {
|
||||
if !strings.Contains(err.Error(), "sha256:"+digest) {
|
||||
t.Errorf("the refusal does not name sha256:%s: %v", digest, err)
|
||||
}
|
||||
}
|
||||
if len(r.archives) != 0 {
|
||||
t.Fatalf("something was published: %v", r.archives)
|
||||
}
|
||||
kept, _ := filepath.Glob(filepath.Join(upstreamDir(workspace), "*"))
|
||||
if len(kept) != 0 {
|
||||
t.Fatalf("the refused download was kept: %v", kept)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUpstreamArchiveCompressedWithXzIsRead(t *testing.T) {
|
||||
// Betterbird publishes its Linux release as .tar.xz; the build seat's image carries xz for it.
|
||||
if _, err := exec.LookPath("xz"); err != nil {
|
||||
t.Skip("NOT TESTED here: no xz on this machine (the build seat's image carries it)")
|
||||
}
|
||||
plain := tarOf(t, []tarEntry{{name: "betterbird/betterbird", body: "#!/bin/sh\n", mode: 0o755}})
|
||||
cmd := exec.Command("xz", "-zc")
|
||||
cmd.Stdin = bytes.NewReader(plain)
|
||||
compressed, err := cmd.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v := &vendor{files: map[string][]byte{"/r.tar.xz": compressed}}
|
||||
base := serveVendor(t, v)
|
||||
packed, err := upstreamArchive(context.Background(), t.TempDir(), catalogue.Artifact{Name: "archive", Kind: "archive",
|
||||
URL: base + "/r.tar.xz", SHA256: sumHex(compressed), Version: "1", Within: "betterbird"}, func(string, string, ...any) {})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var names []string
|
||||
for n := range entriesOf(t, packed) {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
if strings.Join(names, ",") != "betterbird" {
|
||||
t.Fatalf("the xz release unpacked to %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUpstreamTarWithAGlobalHeaderNestedWithinAndTrailingDataIsRead(t *testing.T) {
|
||||
// git archive writes a pax global header; a release may unpack under a/b with a/ listed first; xz may be
|
||||
// followed by bytes after the tar's end, which must be read before xz is waited for.
|
||||
var raw bytes.Buffer
|
||||
w := tar.NewWriter(&raw)
|
||||
w.WriteHeader(&tar.Header{Typeflag: tar.TypeXGlobalHeader, Name: "pax_global_header", PAXRecords: map[string]string{"comment": "abc"}})
|
||||
w.WriteHeader(&tar.Header{Name: "a/", Typeflag: tar.TypeDir, Mode: 0o755})
|
||||
w.WriteHeader(&tar.Header{Name: "a/b/", Typeflag: tar.TypeDir, Mode: 0o755})
|
||||
w.WriteHeader(&tar.Header{Name: "a/b/f", Typeflag: tar.TypeReg, Mode: 0o644, Size: 1})
|
||||
w.Write([]byte("x"))
|
||||
w.Close()
|
||||
body := gzipped(t, raw.Bytes())
|
||||
v := &vendor{files: map[string][]byte{"/r.tar.gz": body}}
|
||||
base := serveVendor(t, v)
|
||||
packed, err := upstreamArchive(context.Background(), t.TempDir(), catalogue.Artifact{Name: "a", Kind: "archive",
|
||||
URL: base + "/r.tar.gz", SHA256: sumHex(body), Version: "1", Within: "a/b"}, func(string, string, ...any) {})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if e := entriesOf(t, packed); len(e) != 1 || e["f"] == "" {
|
||||
t.Fatalf("%v", e)
|
||||
}
|
||||
if _, err := exec.LookPath("xz"); err != nil {
|
||||
t.Skip("NOT TESTED here: no xz")
|
||||
}
|
||||
// A megabyte after the tar's end: more than a pipe holds.
|
||||
trailing := append(append([]byte{}, raw.Bytes()...), bytes.Repeat([]byte{0}, 1<<20)...)
|
||||
cmd := exec.Command("xz", "-zc")
|
||||
cmd.Stdin = bytes.NewReader(trailing)
|
||||
compressed, err := cmd.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v.files["/r.tar.xz"] = compressed
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if _, err := upstreamArchive(ctx, t.TempDir(), catalogue.Artifact{Name: "a", Kind: "archive", URL: base + "/r.tar.xz",
|
||||
SHA256: sumHex(compressed), Version: "1", Within: "a/b"}, func(string, string, ...any) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -15,13 +15,10 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
@@ -153,98 +150,6 @@ func pinnedManifest(base, sum string) string {
|
||||
"resources":[{"id":"installation","type":"archive","path":"/opt/betterbird","artifact":"archive"}]}`
|
||||
}
|
||||
|
||||
func TestAnArchivePinnedByURLIsFetchedCheckedAndPackedWithoutItsOwnDirectory(t *testing.T) {
|
||||
body := release(t)
|
||||
v := &vendor{files: map[string][]byte{"/release.tar.gz": body}}
|
||||
base := serveVendor(t, v)
|
||||
r, workspace := aRepository(t, pinnedManifest(base, sumHex(body)), nil)
|
||||
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/catalogue.git", "", "", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
digest, _ := got.Manifest.Resources[0]["digest"].(string)
|
||||
if !strings.HasPrefix(digest, "sha256:") || r.archives["betterbird/archive"] != digest {
|
||||
t.Fatalf("the release was not published and pinned as the module's archive: %v, published %v",
|
||||
got.Manifest.Resources[0], r.archives)
|
||||
}
|
||||
|
||||
packed, err := upstreamArchive(context.Background(), workspace, catalogue.Artifact{Name: "archive", Kind: "archive",
|
||||
URL: base + "/release.tar.gz", SHA256: sumHex(body), Version: "153.4.0esr-bb10", Within: "betterbird"},
|
||||
func(string, string, ...any) {})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if "sha256:"+sumHex(packed) != digest {
|
||||
t.Fatalf("a second packing of one pin gave another digest: %s and %s", digest, sumHex(packed))
|
||||
}
|
||||
entries := entriesOf(t, packed)
|
||||
want := map[string]string{
|
||||
"application.ini": "[App]\nVersion=153.4.0\n -rw-r--r--",
|
||||
"betterbird": "#!/bin/sh\n -rwxr-xr-x",
|
||||
"defaults/pref/channel-prefs.js": "pref(\"app.update.channel\", \"default\");\n -rw-r--r--",
|
||||
}
|
||||
if len(entries) != len(want) {
|
||||
t.Fatalf("the archive holds %v, want %v", entries, want)
|
||||
}
|
||||
for name, w := range want {
|
||||
if entries[name] != w {
|
||||
t.Errorf("%s is %q in the archive, want %q", name, entries[name], w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUpstreamFileIsFetchedOnceAndKeptByItsDigest(t *testing.T) {
|
||||
// The vendor of an add-on serves only its newest version at one address: a pin that was fetched once
|
||||
// must still build after the address moved on.
|
||||
body := release(t)
|
||||
v := &vendor{files: map[string][]byte{"/release.tar.gz": body}}
|
||||
base := serveVendor(t, v)
|
||||
r, workspace := aRepository(t, pinnedManifest(base, sumHex(body)), nil)
|
||||
first, err := Build(context.Background(), r.run, r, "https://forge.invalid/catalogue.git", "", "", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v.files["/release.tar.gz"] = []byte("the vendor's next release")
|
||||
second, err := Build(context.Background(), r.run, r, "https://forge.invalid/catalogue.git", "", "", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("a pin fetched before failed to build once the address served something else: %v", err)
|
||||
}
|
||||
if v.asked != 1 {
|
||||
t.Fatalf("the vendor was asked %d times for one pin, want once", v.asked)
|
||||
}
|
||||
if first.Manifest.Resources[0]["digest"] != second.Manifest.Resources[0]["digest"] {
|
||||
t.Fatal("two builds of one pin gave two digests")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUpstreamFileWhoseDownloadDoesNotMatchItsPinFailsAndNamesBothDigests(t *testing.T) {
|
||||
pinned := release(t)
|
||||
served := gzipped(t, tarOf(t, []tarEntry{{name: "betterbird/betterbird", body: "not what was reviewed", mode: 0o755}}))
|
||||
v := &vendor{files: map[string][]byte{"/release.tar.gz": served}}
|
||||
base := serveVendor(t, v)
|
||||
r, workspace := aRepository(t, pinnedManifest(base, sumHex(pinned)), nil)
|
||||
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/catalogue.git", "", "", workspace, nil,
|
||||
Npmrc{}, GitCredential{}, nil)
|
||||
if err == nil {
|
||||
t.Fatal("a download that is not the pinned file was built")
|
||||
}
|
||||
for _, digest := range []string{sumHex(pinned), sumHex(served)} {
|
||||
if !strings.Contains(err.Error(), "sha256:"+digest) {
|
||||
t.Errorf("the refusal does not name sha256:%s: %v", digest, err)
|
||||
}
|
||||
}
|
||||
if len(r.archives) != 0 {
|
||||
t.Fatalf("something was published: %v", r.archives)
|
||||
}
|
||||
kept, _ := filepath.Glob(filepath.Join(upstreamDir(workspace), "*"))
|
||||
if len(kept) != 0 {
|
||||
t.Fatalf("the refused download was kept: %v", kept)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUpstreamFileThatIsNotAnArchiveIsKeptWholeUnderItsName(t *testing.T) {
|
||||
xpi := []byte("PK\x03\x04 an add-on")
|
||||
v := &vendor{files: map[string][]byte{"/owl.xpi": xpi}}
|
||||
@@ -282,78 +187,10 @@ func TestAnUpstreamArchiveHoldingALinkOrAnEntryOutsideItsDirectoryIsRefused(t *t
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUpstreamArchiveCompressedWithXzIsRead(t *testing.T) {
|
||||
// Betterbird publishes its Linux release as .tar.xz; the build seat's image carries xz for it.
|
||||
if _, err := exec.LookPath("xz"); err != nil {
|
||||
t.Skip("NOT TESTED here: no xz on this machine (the build seat's image carries it)")
|
||||
}
|
||||
plain := tarOf(t, []tarEntry{{name: "betterbird/betterbird", body: "#!/bin/sh\n", mode: 0o755}})
|
||||
cmd := exec.Command("xz", "-zc")
|
||||
cmd.Stdin = bytes.NewReader(plain)
|
||||
compressed, err := cmd.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v := &vendor{files: map[string][]byte{"/r.tar.xz": compressed}}
|
||||
base := serveVendor(t, v)
|
||||
packed, err := upstreamArchive(context.Background(), t.TempDir(), catalogue.Artifact{Name: "archive", Kind: "archive",
|
||||
URL: base + "/r.tar.xz", SHA256: sumHex(compressed), Version: "1", Within: "betterbird"}, func(string, string, ...any) {})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var names []string
|
||||
for n := range entriesOf(t, packed) {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
if strings.Join(names, ",") != "betterbird" {
|
||||
t.Fatalf("the xz release unpacked to %v", names)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUpstreamTarWithAGlobalHeaderNestedWithinAndTrailingDataIsRead(t *testing.T) {
|
||||
// git archive writes a pax global header; a release may unpack under a/b with a/ listed first; xz may be
|
||||
// followed by bytes after the tar's end, which must be read before xz is waited for.
|
||||
var raw bytes.Buffer
|
||||
w := tar.NewWriter(&raw)
|
||||
w.WriteHeader(&tar.Header{Typeflag: tar.TypeXGlobalHeader, Name: "pax_global_header", PAXRecords: map[string]string{"comment": "abc"}})
|
||||
w.WriteHeader(&tar.Header{Name: "a/", Typeflag: tar.TypeDir, Mode: 0o755})
|
||||
w.WriteHeader(&tar.Header{Name: "a/b/", Typeflag: tar.TypeDir, Mode: 0o755})
|
||||
w.WriteHeader(&tar.Header{Name: "a/b/f", Typeflag: tar.TypeReg, Mode: 0o644, Size: 1})
|
||||
w.Write([]byte("x"))
|
||||
w.Close()
|
||||
body := gzipped(t, raw.Bytes())
|
||||
v := &vendor{files: map[string][]byte{"/r.tar.gz": body}}
|
||||
base := serveVendor(t, v)
|
||||
packed, err := upstreamArchive(context.Background(), t.TempDir(), catalogue.Artifact{Name: "a", Kind: "archive",
|
||||
URL: base + "/r.tar.gz", SHA256: sumHex(body), Version: "1", Within: "a/b"}, func(string, string, ...any) {})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if e := entriesOf(t, packed); len(e) != 1 || e["f"] == "" {
|
||||
t.Fatalf("%v", e)
|
||||
}
|
||||
if _, err := exec.LookPath("xz"); err != nil {
|
||||
t.Skip("NOT TESTED here: no xz")
|
||||
}
|
||||
// A megabyte after the tar's end: more than a pipe holds.
|
||||
trailing := append(append([]byte{}, raw.Bytes()...), bytes.Repeat([]byte{0}, 1<<20)...)
|
||||
cmd := exec.Command("xz", "-zc")
|
||||
cmd.Stdin = bytes.NewReader(trailing)
|
||||
compressed, err := cmd.Output()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v.files["/r.tar.xz"] = compressed
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if _, err := upstreamArchive(ctx, t.TempDir(), catalogue.Artifact{Name: "a", Kind: "archive", URL: base + "/r.tar.xz",
|
||||
SHA256: sumHex(compressed), Version: "1", Within: "a/b"}, func(string, string, ...any) {}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnUpstreamArchiveWithTooManyEntriesIsRefused(t *testing.T) {
|
||||
before := maxEntries
|
||||
maxEntries = 100
|
||||
t.Cleanup(func() { maxEntries = before })
|
||||
var raw bytes.Buffer
|
||||
w := tar.NewWriter(&raw)
|
||||
for i := 0; i <= maxEntries; i++ {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
//go:build integration
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAnXzFileOfAFewDeltaBlocksAndTwoStreamsIsRead(t *testing.T) {
|
||||
// The streams this test builds are ones the reader decodes, so what the walk refuses below is the
|
||||
// same framing the reader follows.
|
||||
body := bytes.Repeat([]byte("betterbird "), 300)
|
||||
whole := tarHolding(t, body)
|
||||
half := len(whole) / 2
|
||||
for name, file := range map[string][]byte{
|
||||
"one stream of delta blocks": xzStream(whole, 512, true, -1),
|
||||
"two streams with padding": append(append(xzStream(whole[:half], 700, true, -1), 0, 0, 0, 0),
|
||||
xzStream(whole[half:], 100, false, -1)...),
|
||||
} {
|
||||
path := writeTemp(t, file)
|
||||
if xzPath, err := exec.LookPath("xz"); err == nil {
|
||||
if out, err := exec.Command(xzPath, "-t", path).CombinedOutput(); err != nil {
|
||||
t.Fatalf("%s: xz itself refuses the built stream: %v %s", name, err, out)
|
||||
}
|
||||
}
|
||||
into := t.TempDir()
|
||||
if err := unpackUpstream(context.Background(), path, into, "betterbird"); err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(into, "data"))
|
||||
if err != nil || !bytes.Equal(got, body) {
|
||||
t.Fatalf("%s: unpacked %d bytes, want %d: %v", name, len(got), len(body), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"errors"
|
||||
"hash/crc32"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
@@ -125,34 +124,6 @@ func writeTemp(t *testing.T, body []byte) string {
|
||||
return file
|
||||
}
|
||||
|
||||
func TestAnXzFileOfAFewDeltaBlocksAndTwoStreamsIsRead(t *testing.T) {
|
||||
// The streams this test builds are ones the reader decodes, so what the walk refuses below is the
|
||||
// same framing the reader follows.
|
||||
body := bytes.Repeat([]byte("betterbird "), 300)
|
||||
whole := tarHolding(t, body)
|
||||
half := len(whole) / 2
|
||||
for name, file := range map[string][]byte{
|
||||
"one stream of delta blocks": xzStream(whole, 512, true, -1),
|
||||
"two streams with padding": append(append(xzStream(whole[:half], 700, true, -1), 0, 0, 0, 0),
|
||||
xzStream(whole[half:], 100, false, -1)...),
|
||||
} {
|
||||
path := writeTemp(t, file)
|
||||
if xzPath, err := exec.LookPath("xz"); err == nil {
|
||||
if out, err := exec.Command(xzPath, "-t", path).CombinedOutput(); err != nil {
|
||||
t.Fatalf("%s: xz itself refuses the built stream: %v %s", name, err, out)
|
||||
}
|
||||
}
|
||||
into := t.TempDir()
|
||||
if err := unpackUpstream(context.Background(), path, into, "betterbird"); err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(into, "data"))
|
||||
if err != nil || !bytes.Equal(got, body) {
|
||||
t.Fatalf("%s: unpacked %d bytes, want %d: %v", name, len(got), len(body), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnXzFileOfManyDeltaBlocksIsRefusedBeforeItIsDecoded(t *testing.T) {
|
||||
// Review 68 on pull request 264: `xz --block-size=1 --delta` of a 200 kB tar kept about 67 MiB live in
|
||||
// the reader, one delta decoder per block. Its index here lists one block, as a file crafted to pass a
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
//go:build integration
|
||||
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestThePOSIXEnvironmentSourcedTwiceLeavesPATHAsOnce(t *testing.T) {
|
||||
sh, err := exec.LookPath("sh")
|
||||
if err != nil {
|
||||
t.Skip("no sh on this machine")
|
||||
}
|
||||
script := "PATH=/usr/bin:/bin\n" + composedPOSIX + "once=$PATH\n" + composedPOSIX +
|
||||
`[ "$PATH" = "$once" ] || { echo "changed: $once -> $PATH"; exit 1; }` + "\n" +
|
||||
`echo "$PATH"; echo "$GOPATH"`
|
||||
out, err := exec.Command(sh, "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("sourcing twice: %v\n%s", err, out)
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
want := "/home/op/go/bin:/usr/local/go/bin:/home/op/.local/bin:/home/op/bin:/usr/bin:/bin:/opt/agent/bin:/opt/scripts"
|
||||
if lines[0] != want {
|
||||
t.Fatalf("PATH is %s, not %s", lines[0], want)
|
||||
}
|
||||
if lines[1] != "/home/op/go" {
|
||||
t.Fatalf("GOPATH was not exported: %q", lines[1])
|
||||
}
|
||||
// And an entry the account already has stays where it is, and once.
|
||||
out, err = exec.Command(sh, "-c", "PATH=/opt/scripts:/usr/bin\n"+composedPOSIX+`echo "$PATH"`).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("%v\n%s", err, out)
|
||||
}
|
||||
if got := strings.TrimSpace(string(out)); got !=
|
||||
"/home/op/go/bin:/usr/local/go/bin:/home/op/.local/bin:/home/op/bin:/opt/scripts:/usr/bin:/opt/agent/bin" {
|
||||
t.Fatalf("an entry already on PATH was added again or moved: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheServiceManagerReadsTheSystemdRenderingAsMeant(t *testing.T) {
|
||||
generator := "/usr/lib/systemd/user-environment-generators/30-systemd-environment-d-generator"
|
||||
if _, err := os.Stat(generator); err != nil {
|
||||
t.Skip("NOT JUDGED: no environment.d generator on this machine, so the service manager's reading " +
|
||||
"of the rendering is judged only where systemd runs; the rendering itself is held byte for byte " +
|
||||
"by TestTheEnvironmentRendersForTheServiceManagerByteForByte")
|
||||
}
|
||||
const base = "/usr/bin:/bin"
|
||||
read := func(conf string) map[string]string {
|
||||
t.Helper()
|
||||
config := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(config, "environment.d"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conf != "" {
|
||||
if err := os.WriteFile(filepath.Join(config, "environment.d", "50-mesh.conf"), []byte(conf), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
cmd := exec.Command(generator)
|
||||
cmd.Env = []string{"PATH=" + base, "HOME=" + config, "XDG_CONFIG_HOME=" + config}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("%v\n%s", err, out)
|
||||
}
|
||||
vars := map[string]string{}
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
if k, v, ok := strings.Cut(line, "="); ok {
|
||||
vars[k] = v
|
||||
}
|
||||
}
|
||||
return vars
|
||||
}
|
||||
machine, read50 := read(""), read(composedSystemd)
|
||||
|
||||
// What the machine's own files do to PATH: nothing, or append to it. One that replaces or prepends
|
||||
// leaves nothing this test can say about the mesh's file, and says so rather than guess.
|
||||
added, ok := strings.CutPrefix(machine["PATH"], base)
|
||||
if machine["PATH"] != "" && !ok {
|
||||
t.Skipf("NOT JUDGED: this machine's own environment.d sets PATH to %s, not %s with something after it, so "+
|
||||
"what the mesh's file adds cannot be told apart from it", machine["PATH"], base)
|
||||
}
|
||||
want := "/home/op/go/bin:/usr/local/go/bin:/home/op/.local/bin:/home/op/bin:/usr/bin:/bin:/opt/agent/bin:/opt/scripts" + added
|
||||
if read50["PATH"] != want || read50["GOPATH"] != "/home/op/go" {
|
||||
t.Fatalf("the service manager read PATH=%s GOPATH=%s, not PATH=%s GOPATH=/home/op/go (the machine's own "+
|
||||
"files add %q to PATH)", read50["PATH"], read50["GOPATH"], want, added)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -101,36 +98,6 @@ func TestTheEnvironmentRendersForTheServiceManagerByteForByte(t *testing.T) {
|
||||
// Sourcing twice changes nothing (ADR 0203 §3): a login shell that starts another reads the file
|
||||
// again, and a PATH that grew each time would be the symptom. Run by a real `sh`, because the claim
|
||||
// is about what a shell does with the file, not about what the file looks like.
|
||||
func TestThePOSIXEnvironmentSourcedTwiceLeavesPATHAsOnce(t *testing.T) {
|
||||
sh, err := exec.LookPath("sh")
|
||||
if err != nil {
|
||||
t.Skip("no sh on this machine")
|
||||
}
|
||||
script := "PATH=/usr/bin:/bin\n" + composedPOSIX + "once=$PATH\n" + composedPOSIX +
|
||||
`[ "$PATH" = "$once" ] || { echo "changed: $once -> $PATH"; exit 1; }` + "\n" +
|
||||
`echo "$PATH"; echo "$GOPATH"`
|
||||
out, err := exec.Command(sh, "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("sourcing twice: %v\n%s", err, out)
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
||||
want := "/home/op/go/bin:/usr/local/go/bin:/home/op/.local/bin:/home/op/bin:/usr/bin:/bin:/opt/agent/bin:/opt/scripts"
|
||||
if lines[0] != want {
|
||||
t.Fatalf("PATH is %s, not %s", lines[0], want)
|
||||
}
|
||||
if lines[1] != "/home/op/go" {
|
||||
t.Fatalf("GOPATH was not exported: %q", lines[1])
|
||||
}
|
||||
// And an entry the account already has stays where it is, and once.
|
||||
out, err = exec.Command(sh, "-c", "PATH=/opt/scripts:/usr/bin\n"+composedPOSIX+`echo "$PATH"`).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("%v\n%s", err, out)
|
||||
}
|
||||
if got := strings.TrimSpace(string(out)); got !=
|
||||
"/home/op/go/bin:/usr/local/go/bin:/home/op/.local/bin:/home/op/bin:/opt/scripts:/usr/bin:/opt/agent/bin" {
|
||||
t.Fatalf("an entry already on PATH was added again or moved: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The environment.d rendering, read by the service manager's own generator where this machine has
|
||||
// one — the same reader an account's user manager runs, so the PATH it composes is the one asserted.
|
||||
@@ -143,54 +110,6 @@ func TestThePOSIXEnvironmentSourcedTwiceLeavesPATHAsOnce(t *testing.T) {
|
||||
// A machine without the generator — the build seat's toolchain image holds no systemd — cannot judge
|
||||
// this and says so: there the rendering is held byte for byte by
|
||||
// TestTheEnvironmentRendersForTheServiceManagerByteForByte, and this reading only where systemd runs.
|
||||
func TestTheServiceManagerReadsTheSystemdRenderingAsMeant(t *testing.T) {
|
||||
generator := "/usr/lib/systemd/user-environment-generators/30-systemd-environment-d-generator"
|
||||
if _, err := os.Stat(generator); err != nil {
|
||||
t.Skip("NOT JUDGED: no environment.d generator on this machine, so the service manager's reading " +
|
||||
"of the rendering is judged only where systemd runs; the rendering itself is held byte for byte " +
|
||||
"by TestTheEnvironmentRendersForTheServiceManagerByteForByte")
|
||||
}
|
||||
const base = "/usr/bin:/bin"
|
||||
read := func(conf string) map[string]string {
|
||||
t.Helper()
|
||||
config := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(config, "environment.d"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conf != "" {
|
||||
if err := os.WriteFile(filepath.Join(config, "environment.d", "50-mesh.conf"), []byte(conf), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
cmd := exec.Command(generator)
|
||||
cmd.Env = []string{"PATH=" + base, "HOME=" + config, "XDG_CONFIG_HOME=" + config}
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("%v\n%s", err, out)
|
||||
}
|
||||
vars := map[string]string{}
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
if k, v, ok := strings.Cut(line, "="); ok {
|
||||
vars[k] = v
|
||||
}
|
||||
}
|
||||
return vars
|
||||
}
|
||||
machine, read50 := read(""), read(composedSystemd)
|
||||
|
||||
// What the machine's own files do to PATH: nothing, or append to it. One that replaces or prepends
|
||||
// leaves nothing this test can say about the mesh's file, and says so rather than guess.
|
||||
added, ok := strings.CutPrefix(machine["PATH"], base)
|
||||
if machine["PATH"] != "" && !ok {
|
||||
t.Skipf("NOT JUDGED: this machine's own environment.d sets PATH to %s, not %s with something after it, so "+
|
||||
"what the mesh's file adds cannot be told apart from it", machine["PATH"], base)
|
||||
}
|
||||
want := "/home/op/go/bin:/usr/local/go/bin:/home/op/.local/bin:/home/op/bin:/usr/bin:/bin:/opt/agent/bin:/opt/scripts" + added
|
||||
if read50["PATH"] != want || read50["GOPATH"] != "/home/op/go" {
|
||||
t.Fatalf("the service manager read PATH=%s GOPATH=%s, not PATH=%s GOPATH=/home/op/go (the machine's own "+
|
||||
"files add %q to PATH)", read50["PATH"], read50["GOPATH"], want, added)
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing contributed renders nothing, in both formats — not an empty `export PATH`.
|
||||
func TestNoContributionsRenderNothing(t *testing.T) {
|
||||
|
||||
@@ -75,8 +75,9 @@ func TestEveryCatalogueManifestDeclaresWhatItMounts(t *testing.T) {
|
||||
if len(files) == 0 {
|
||||
t.Fatal("no manifests in the catalogue, so this proved nothing")
|
||||
}
|
||||
root := beside.Dir(t, "mesh-catalog") // once: it copies the captured tree
|
||||
for _, file := range files {
|
||||
if _, err := ReadTreeManifest(beside.Dir(t, "mesh-catalog"), filepath.Dir(file)); err != nil {
|
||||
if _, err := ReadTreeManifest(root, filepath.Dir(file)); err != nil {
|
||||
t.Errorf("%s: %v", filepath.Base(filepath.Dir(file)), err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func aMeshWith(t *testing.T, manifests ...catalogue.Manifest) (*Inventory, context.Context) {
|
||||
t.Helper()
|
||||
inv := ForTest(t)
|
||||
ctx := context.Background()
|
||||
for _, m := range manifests {
|
||||
if err := inv.RegisterModule(ctx, m, Source{Repository: "/r"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return inv, ctx
|
||||
}
|
||||
|
||||
func TestAnAssignedModuleBecomesAUserWithWhatItDeclared(t *testing.T) {
|
||||
// It declares where its account is delivered: a module with no own secret named broker can never
|
||||
// be issued one, and is no user at all (novox/hq issue 195) — the case asserted below.
|
||||
shop := catalogue.Manifest{
|
||||
Module: "shop", Version: "1",
|
||||
Emits: []string{"order.placed"}, Tools: []string{"price"},
|
||||
Uses: []string{"telegram-sender"},
|
||||
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/run/broker"}},
|
||||
}
|
||||
quiet := catalogue.Manifest{Module: "quiet", Version: "1", Emits: []string{"thing.happened"}}
|
||||
inv, ctx := aMeshWith(t, theSeatDeclarer(), shop, quiet)
|
||||
if _, err := inv.AddNode(ctx, "one"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, module := range []string{"shop", "quiet"} {
|
||||
if _, err := inv.Assign(ctx, "one", module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
records, err := inv.BusRecords(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var on []broker.Declared
|
||||
for _, d := range records.Assigned["one"] {
|
||||
if d.Module == "shop" {
|
||||
on = append(on, d)
|
||||
}
|
||||
}
|
||||
if len(on) != 1 || on[0].NoAccount {
|
||||
t.Fatalf("the machine's modules read as %+v", records.Assigned["one"])
|
||||
}
|
||||
if len(on[0].Uses) != 1 || on[0].Uses[0].Accepts[0] != "send" {
|
||||
t.Fatalf("the seat it uses carries no protocol: %+v — so it would be granted nothing on a "+
|
||||
"seat it was assigned to send to", on[0].Uses)
|
||||
}
|
||||
if len(on[0].Serves) != 1 || on[0].Serves[0] != "price" {
|
||||
t.Fatalf("its tools read as %v, and a module that cannot subscribe its own tool subject "+
|
||||
"serves nothing", on[0].Serves)
|
||||
}
|
||||
|
||||
// And it derives into a user the server would accept.
|
||||
users, err := broker.Users(records)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var found bool
|
||||
for _, u := range users {
|
||||
if u.Username() != "one.shop" {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
perms, err := broker.PermissionsFor(u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Its tools are every one under its own name — the list in the manifest is a person's
|
||||
// vocabulary for asking, not the module's permission to answer.
|
||||
if !granted(perms.Publish, "mesh.mod.shop.event.order.placed") ||
|
||||
!granted(perms.Publish, "mesh.seat.telegram-sender.accept.send") ||
|
||||
!granted(perms.Subscribe, "mesh.mod.shop.tool.>") {
|
||||
t.Fatalf("one.shop's authority is not what it declared: %+v", perms)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("no user was derived for the assigned module")
|
||||
}
|
||||
for _, u := range users {
|
||||
if u.Username() == "one.quiet" {
|
||||
t.Fatal("a module with nowhere to read an account was made a user (novox/hq issue 195)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnlyAMachineWithALiveTokenHasAnEnrolmentUser(t *testing.T) {
|
||||
inv, ctx := aMeshWith(t)
|
||||
for _, name := range []string{"live", "expired", "none"} {
|
||||
if _, err := inv.AddNode(ctx, name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := inv.IssueToken(ctx, "live", time.Hour); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Briefly, then waited out: a token with no lifetime is refused at issue, which is the right
|
||||
// refusal and leaves this as the way to have an expired one.
|
||||
if _, err := inv.IssueToken(ctx, "expired", 10*time.Millisecond); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
records, err := inv.BusRecords(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Join(records.Enrolling, ",") != "live" {
|
||||
t.Fatalf("machines with a live token read as %v", records.Enrolling)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssuingATokenRecordsTheAccountItIsThePasswordOf(t *testing.T) {
|
||||
inv, ctx := aMeshWith(t)
|
||||
if _, err := inv.AddNode(ctx, "joiner"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
issued, err := inv.IssueToken(ctx, "joiner", time.Hour)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
name := broker.Principal{Kind: broker.KindEnrolment, Node: "joiner"}.Username()
|
||||
users, err := inv.BusUsers(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
user, has := users[name]
|
||||
if !has {
|
||||
t.Fatalf("no bus account for %q; the composer would leave the enrolment out and the "+
|
||||
"machine would be refused before the mesh heard of it: %v", name, users)
|
||||
}
|
||||
if user.Kind != BusEnrolment || user.Node != "joiner" {
|
||||
t.Errorf("the account is %+v, not this node's enrolment", user)
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(issued.Secret)); err != nil {
|
||||
t.Error("the account does not accept the secret the token carries, so presenting the " +
|
||||
"token would be refused by the server")
|
||||
}
|
||||
|
||||
// And the composition contains it, which is the thing the server reads.
|
||||
records, err := inv.BusRecords(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
derived, err := broker.Users(records)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hashes := map[string]string{}
|
||||
for n, u := range users {
|
||||
hashes[n] = u.PasswordHash
|
||||
}
|
||||
_, missing := broker.WithPasswords(derived, hashes)
|
||||
for _, m := range missing {
|
||||
if m == name {
|
||||
t.Fatal("the enrolment user is composed without a password, which is a user nobody can be")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheBussOwnModuleBecomesTheSnapshotUser(t *testing.T) {
|
||||
bus := catalogue.Manifest{
|
||||
Module: "nats", Version: "1",
|
||||
Provides: []catalogue.Offer{{Name: "mesh-bus", Scope: catalogue.ScopeMesh}},
|
||||
Claims: []catalogue.Claim{{Name: catalogue.BrokerSeat, Scope: catalogue.ScopeMesh}},
|
||||
Tools: []string{"nats_streams"},
|
||||
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/run/broker"}},
|
||||
}
|
||||
shop := catalogue.Manifest{Module: "shop", Version: "1", Emits: []string{"order.placed"},
|
||||
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/run/broker"}}}
|
||||
inv, ctx := aMeshWith(t, bus, shop)
|
||||
if _, err := inv.AddNode(ctx, "one"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, module := range []string{"nats", "shop"} {
|
||||
if _, err := inv.Assign(ctx, "one", module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
records, err := inv.BusRecords(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
users, err := broker.Users(records)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, u := range users {
|
||||
perms, err := broker.PermissionsFor(u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
snapshots := granted(perms.Publish, "$JS.API.STREAM.SNAPSHOT.*")
|
||||
switch u.Username() {
|
||||
case "one.nats":
|
||||
seen[u.Username()] = true
|
||||
if !snapshots || len(perms.Publish) != len(broker.BusSnapshotGrants.Publish) {
|
||||
t.Fatalf("the bus's own user is granted %v, not the snapshot API alone", perms.Publish)
|
||||
}
|
||||
case "one.shop":
|
||||
seen[u.Username()] = true
|
||||
if snapshots {
|
||||
t.Fatal("a module that is not the bus may snapshot it")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !seen["one.nats"] || !seen["one.shop"] {
|
||||
t.Fatalf("users derived: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestABarIsGrantedTheStateItsMachinesModulesOfferIt(t *testing.T) {
|
||||
bar := catalogue.Manifest{Module: "a-bar", Version: "1",
|
||||
Claims: []catalogue.Claim{{Name: catalogue.BarSeat, Scope: catalogue.ScopeNode}}}
|
||||
power := catalogue.Manifest{Module: "power", Version: "1",
|
||||
State: []catalogue.StateDeclaration{{Name: "draw"}},
|
||||
Contributions: []catalogue.SeatContribution{{Seat: catalogue.BarSeat, Kind: catalogue.BarKindBlock,
|
||||
Data: map[string]any{"bar": "bottom", "place": "status", "shows": "state",
|
||||
"options": map[string]any{"state": "power.draw"}}}}}
|
||||
inv, ctx := aMeshWith(t, bar, power)
|
||||
for node, modules := range map[string][]string{"laptop": {"a-bar", "power"}, "desk": {"a-bar"}} {
|
||||
if _, err := inv.AddNode(ctx, node); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, module := range modules {
|
||||
if _, err := inv.Assign(ctx, node, module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
records, err := inv.BusRecords(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reads := func(node, module string) []broker.KeyedRead {
|
||||
for _, d := range records.Assigned[node] {
|
||||
if d.Module == module {
|
||||
if len(d.Reads) != 0 {
|
||||
t.Fatalf("%s on %s reads whole buckets: %v", module, node, d.Reads)
|
||||
}
|
||||
return d.KeyedReads
|
||||
}
|
||||
}
|
||||
t.Fatalf("%s is not on %s", module, node)
|
||||
return nil
|
||||
}
|
||||
// The laptop's own key, and no other (novox/hq ADR 0260).
|
||||
if got := reads("laptop", "a-bar"); len(got) != 1 || got[0] != (broker.KeyedRead{Read: "power.draw", Key: "laptop"}) {
|
||||
t.Fatalf("the laptop's bar reads %v", got)
|
||||
}
|
||||
if got := reads("desk", "a-bar"); len(got) != 0 {
|
||||
t.Fatalf("a bar with no power beside it reads %v", got)
|
||||
}
|
||||
if got := reads("laptop", "power"); len(got) != 0 {
|
||||
t.Fatalf("power reads %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheProvisionerClientsMachineHasAnAccountOfItsOwn(t *testing.T) {
|
||||
provisioner := catalogue.Manifest{
|
||||
Module: "mesh-provisioner", Version: "1",
|
||||
DefinesSeats: []catalogue.SeatDeclaration{{Name: broker.NodeProvisionerSeat, Scope: catalogue.ScopeNode,
|
||||
Serves: []catalogue.Verb{{Name: "get"}}}},
|
||||
Claims: []catalogue.Claim{{Name: broker.NodeProvisionerSeat, Scope: catalogue.ScopeNode}},
|
||||
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/run/broker"},
|
||||
broker.ProvisionerSecret: {Path: "/run/provisioner-broker"}},
|
||||
}
|
||||
unread := provisioner
|
||||
unread.Module = "provisioner-unread"
|
||||
unread.DefinesSeats = nil
|
||||
unread.OwnSecrets = catalogue.OwnSecrets{"broker": {Path: "/run/broker"}}
|
||||
inv, ctx := aMeshWith(t, provisioner, unread)
|
||||
for node, module := range map[string]string{"one": "mesh-provisioner", "two": "provisioner-unread"} {
|
||||
if _, err := inv.AddNode(ctx, node); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := inv.Assign(ctx, node, module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
records, err := inv.BusRecords(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
users, err := broker.Users(records)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var clients []string
|
||||
for _, u := range users {
|
||||
if u.Kind == broker.KindProvisioner {
|
||||
clients = append(clients, u.Username()+" for "+u.Module)
|
||||
}
|
||||
}
|
||||
if strings.Join(clients, ",") != "provisioner.one for mesh-provisioner" {
|
||||
t.Fatalf("the provisioner clients' accounts are %v, want one for the machine whose module reads it", clients)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/broker"
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Reading the bus's user list out of the mesh's records, against a real store.
|
||||
@@ -17,18 +14,6 @@ import (
|
||||
// the server reads whatever file it is given, and a module whose user is absent fails on its first
|
||||
// publish with an authorisation error that says nothing about a missing assignment.
|
||||
|
||||
func aMeshWith(t *testing.T, manifests ...catalogue.Manifest) (*Inventory, context.Context) {
|
||||
t.Helper()
|
||||
inv := ForTest(t)
|
||||
ctx := context.Background()
|
||||
for _, m := range manifests {
|
||||
if err := inv.RegisterModule(ctx, m, Source{Repository: "/r"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return inv, ctx
|
||||
}
|
||||
|
||||
func theSeatDeclarer() catalogue.Manifest {
|
||||
return catalogue.Manifest{
|
||||
Module: "telegram", Version: "1",
|
||||
@@ -41,108 +26,9 @@ func theSeatDeclarer() catalogue.Manifest {
|
||||
|
||||
// A module assigned to a machine becomes a user with the authority its manifest declared — and the
|
||||
// protocol of a seat declared by a *different* module, which is the whole reason a seat exists.
|
||||
func TestAnAssignedModuleBecomesAUserWithWhatItDeclared(t *testing.T) {
|
||||
// It declares where its account is delivered: a module with no own secret named broker can never
|
||||
// be issued one, and is no user at all (novox/hq issue 195) — the case asserted below.
|
||||
shop := catalogue.Manifest{
|
||||
Module: "shop", Version: "1",
|
||||
Emits: []string{"order.placed"}, Tools: []string{"price"},
|
||||
Uses: []string{"telegram-sender"},
|
||||
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/run/broker"}},
|
||||
}
|
||||
quiet := catalogue.Manifest{Module: "quiet", Version: "1", Emits: []string{"thing.happened"}}
|
||||
inv, ctx := aMeshWith(t, theSeatDeclarer(), shop, quiet)
|
||||
if _, err := inv.AddNode(ctx, "one"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, module := range []string{"shop", "quiet"} {
|
||||
if _, err := inv.Assign(ctx, "one", module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
records, err := inv.BusRecords(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var on []broker.Declared
|
||||
for _, d := range records.Assigned["one"] {
|
||||
if d.Module == "shop" {
|
||||
on = append(on, d)
|
||||
}
|
||||
}
|
||||
if len(on) != 1 || on[0].NoAccount {
|
||||
t.Fatalf("the machine's modules read as %+v", records.Assigned["one"])
|
||||
}
|
||||
if len(on[0].Uses) != 1 || on[0].Uses[0].Accepts[0] != "send" {
|
||||
t.Fatalf("the seat it uses carries no protocol: %+v — so it would be granted nothing on a "+
|
||||
"seat it was assigned to send to", on[0].Uses)
|
||||
}
|
||||
if len(on[0].Serves) != 1 || on[0].Serves[0] != "price" {
|
||||
t.Fatalf("its tools read as %v, and a module that cannot subscribe its own tool subject "+
|
||||
"serves nothing", on[0].Serves)
|
||||
}
|
||||
|
||||
// And it derives into a user the server would accept.
|
||||
users, err := broker.Users(records)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var found bool
|
||||
for _, u := range users {
|
||||
if u.Username() != "one.shop" {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
perms, err := broker.PermissionsFor(u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Its tools are every one under its own name — the list in the manifest is a person's
|
||||
// vocabulary for asking, not the module's permission to answer.
|
||||
if !granted(perms.Publish, "mesh.mod.shop.event.order.placed") ||
|
||||
!granted(perms.Publish, "mesh.seat.telegram-sender.accept.send") ||
|
||||
!granted(perms.Subscribe, "mesh.mod.shop.tool.>") {
|
||||
t.Fatalf("one.shop's authority is not what it declared: %+v", perms)
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("no user was derived for the assigned module")
|
||||
}
|
||||
for _, u := range users {
|
||||
if u.Username() == "one.quiet" {
|
||||
t.Fatal("a module with nowhere to read an account was made a user (novox/hq issue 195)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A machine holding a live token gets an enrolment user; one whose token is spent or expired does
|
||||
// not. **An enrolment user outliving its token is a right to join that nobody issued.**
|
||||
func TestOnlyAMachineWithALiveTokenHasAnEnrolmentUser(t *testing.T) {
|
||||
inv, ctx := aMeshWith(t)
|
||||
for _, name := range []string{"live", "expired", "none"} {
|
||||
if _, err := inv.AddNode(ctx, name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := inv.IssueToken(ctx, "live", time.Hour); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Briefly, then waited out: a token with no lifetime is refused at issue, which is the right
|
||||
// refusal and leaves this as the way to have an expired one.
|
||||
if _, err := inv.IssueToken(ctx, "expired", 10*time.Millisecond); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
records, err := inv.BusRecords(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Join(records.Enrolling, ",") != "live" {
|
||||
t.Fatalf("machines with a live token read as %v", records.Enrolling)
|
||||
}
|
||||
}
|
||||
|
||||
// A module assigned and absent from the catalogue is refused rather than composed with no authority.
|
||||
//
|
||||
@@ -193,199 +79,14 @@ func granted(all []string, one string) bool {
|
||||
// exactly that string. So this checks the two halves that make the account usable — that a row
|
||||
// exists under the name the composer asks for, and that the secret handed out is what that row
|
||||
// accepts.
|
||||
func TestIssuingATokenRecordsTheAccountItIsThePasswordOf(t *testing.T) {
|
||||
inv, ctx := aMeshWith(t)
|
||||
if _, err := inv.AddNode(ctx, "joiner"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
issued, err := inv.IssueToken(ctx, "joiner", time.Hour)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
name := broker.Principal{Kind: broker.KindEnrolment, Node: "joiner"}.Username()
|
||||
users, err := inv.BusUsers(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
user, has := users[name]
|
||||
if !has {
|
||||
t.Fatalf("no bus account for %q; the composer would leave the enrolment out and the "+
|
||||
"machine would be refused before the mesh heard of it: %v", name, users)
|
||||
}
|
||||
if user.Kind != BusEnrolment || user.Node != "joiner" {
|
||||
t.Errorf("the account is %+v, not this node's enrolment", user)
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(issued.Secret)); err != nil {
|
||||
t.Error("the account does not accept the secret the token carries, so presenting the " +
|
||||
"token would be refused by the server")
|
||||
}
|
||||
|
||||
// And the composition contains it, which is the thing the server reads.
|
||||
records, err := inv.BusRecords(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
derived, err := broker.Users(records)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hashes := map[string]string{}
|
||||
for n, u := range users {
|
||||
hashes[n] = u.PasswordHash
|
||||
}
|
||||
_, missing := broker.WithPasswords(derived, hashes)
|
||||
for _, m := range missing {
|
||||
if m == name {
|
||||
t.Fatal("the enrolment user is composed without a password, which is a user nobody can be")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The module holding mesh-broker is the bus, and its user is the bus's own: granted the snapshot API
|
||||
// for the night's backup and nothing else, whatever tools it declares (novox/hq ADR 0235). Another
|
||||
// module on the same machine is granted none of it.
|
||||
func TestTheBussOwnModuleBecomesTheSnapshotUser(t *testing.T) {
|
||||
bus := catalogue.Manifest{
|
||||
Module: "nats", Version: "1",
|
||||
Provides: []catalogue.Offer{{Name: "mesh-bus", Scope: catalogue.ScopeMesh}},
|
||||
Claims: []catalogue.Claim{{Name: catalogue.BrokerSeat, Scope: catalogue.ScopeMesh}},
|
||||
Tools: []string{"nats_streams"},
|
||||
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/run/broker"}},
|
||||
}
|
||||
shop := catalogue.Manifest{Module: "shop", Version: "1", Emits: []string{"order.placed"},
|
||||
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/run/broker"}}}
|
||||
inv, ctx := aMeshWith(t, bus, shop)
|
||||
if _, err := inv.AddNode(ctx, "one"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, module := range []string{"nats", "shop"} {
|
||||
if _, err := inv.Assign(ctx, "one", module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
records, err := inv.BusRecords(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
users, err := broker.Users(records)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, u := range users {
|
||||
perms, err := broker.PermissionsFor(u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
snapshots := granted(perms.Publish, "$JS.API.STREAM.SNAPSHOT.*")
|
||||
switch u.Username() {
|
||||
case "one.nats":
|
||||
seen[u.Username()] = true
|
||||
if !snapshots || len(perms.Publish) != len(broker.BusSnapshotGrants.Publish) {
|
||||
t.Fatalf("the bus's own user is granted %v, not the snapshot API alone", perms.Publish)
|
||||
}
|
||||
case "one.shop":
|
||||
seen[u.Username()] = true
|
||||
if snapshots {
|
||||
t.Fatal("a module that is not the bus may snapshot it")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !seen["one.nats"] || !seen["one.shop"] {
|
||||
t.Fatalf("users derived: %v", seen)
|
||||
}
|
||||
}
|
||||
|
||||
// The bar's holder reads the state the modules beside it offer in its blocks (novox/hq ADR 0255),
|
||||
// without naming them: granted on the machine where both are, and nowhere else.
|
||||
func TestABarIsGrantedTheStateItsMachinesModulesOfferIt(t *testing.T) {
|
||||
bar := catalogue.Manifest{Module: "a-bar", Version: "1",
|
||||
Claims: []catalogue.Claim{{Name: catalogue.BarSeat, Scope: catalogue.ScopeNode}}}
|
||||
power := catalogue.Manifest{Module: "power", Version: "1",
|
||||
State: []catalogue.StateDeclaration{{Name: "draw"}},
|
||||
Contributions: []catalogue.SeatContribution{{Seat: catalogue.BarSeat, Kind: catalogue.BarKindBlock,
|
||||
Data: map[string]any{"bar": "bottom", "place": "status", "shows": "state",
|
||||
"options": map[string]any{"state": "power.draw"}}}}}
|
||||
inv, ctx := aMeshWith(t, bar, power)
|
||||
for node, modules := range map[string][]string{"laptop": {"a-bar", "power"}, "desk": {"a-bar"}} {
|
||||
if _, err := inv.AddNode(ctx, node); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, module := range modules {
|
||||
if _, err := inv.Assign(ctx, node, module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
records, err := inv.BusRecords(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
reads := func(node, module string) []broker.KeyedRead {
|
||||
for _, d := range records.Assigned[node] {
|
||||
if d.Module == module {
|
||||
if len(d.Reads) != 0 {
|
||||
t.Fatalf("%s on %s reads whole buckets: %v", module, node, d.Reads)
|
||||
}
|
||||
return d.KeyedReads
|
||||
}
|
||||
}
|
||||
t.Fatalf("%s is not on %s", module, node)
|
||||
return nil
|
||||
}
|
||||
// The laptop's own key, and no other (novox/hq ADR 0260).
|
||||
if got := reads("laptop", "a-bar"); len(got) != 1 || got[0] != (broker.KeyedRead{Read: "power.draw", Key: "laptop"}) {
|
||||
t.Fatalf("the laptop's bar reads %v", got)
|
||||
}
|
||||
if got := reads("desk", "a-bar"); len(got) != 0 {
|
||||
t.Fatalf("a bar with no power beside it reads %v", got)
|
||||
}
|
||||
if got := reads("laptop", "power"); len(got) != 0 {
|
||||
t.Fatalf("power reads %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A module running the machine's provisioner client gives that machine a client account of its own, apart from the
|
||||
// module's and the tool runner's (novox/hq ADR 0349 clause 11, design 57 P3); one with nowhere to read the account
|
||||
// gives none.
|
||||
func TestTheProvisionerClientsMachineHasAnAccountOfItsOwn(t *testing.T) {
|
||||
provisioner := catalogue.Manifest{
|
||||
Module: "mesh-provisioner", Version: "1",
|
||||
DefinesSeats: []catalogue.SeatDeclaration{{Name: broker.NodeProvisionerSeat, Scope: catalogue.ScopeNode,
|
||||
Serves: []catalogue.Verb{{Name: "get"}}}},
|
||||
Claims: []catalogue.Claim{{Name: broker.NodeProvisionerSeat, Scope: catalogue.ScopeNode}},
|
||||
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/run/broker"},
|
||||
broker.ProvisionerSecret: {Path: "/run/provisioner-broker"}},
|
||||
}
|
||||
unread := provisioner
|
||||
unread.Module = "provisioner-unread"
|
||||
unread.DefinesSeats = nil
|
||||
unread.OwnSecrets = catalogue.OwnSecrets{"broker": {Path: "/run/broker"}}
|
||||
inv, ctx := aMeshWith(t, provisioner, unread)
|
||||
for node, module := range map[string]string{"one": "mesh-provisioner", "two": "provisioner-unread"} {
|
||||
if _, err := inv.AddNode(ctx, node); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := inv.Assign(ctx, node, module); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
records, err := inv.BusRecords(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
users, err := broker.Users(records)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var clients []string
|
||||
for _, u := range users {
|
||||
if u.Kind == broker.KindProvisioner {
|
||||
clients = append(clients, u.Username()+" for "+u.Module)
|
||||
}
|
||||
}
|
||||
if strings.Join(clients, ",") != "provisioner.one for mesh-provisioner" {
|
||||
t.Fatalf("the provisioner clients' accounts are %v, want one for the machine whose module reads it", clients)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func TestABusPasswordIsReturnedOnceAndOnlyItsHashIsKept(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
ctx := context.Background()
|
||||
|
||||
password, err := inv.MintBusPassword(ctx, aBusUser("shop"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if password == "" {
|
||||
t.Fatal("no password came back, so nothing can be sealed to the module")
|
||||
}
|
||||
|
||||
hash, known, err := inv.BusUserHash(ctx, "one.shop")
|
||||
if err != nil || !known {
|
||||
t.Fatalf("the user was not recorded: %v %v", known, err)
|
||||
}
|
||||
if hash == password {
|
||||
t.Fatal("the store holds the password itself")
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err != nil {
|
||||
t.Fatalf("the recorded hash does not verify the password it was made from: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMintingAgainRotatesRatherThanAddsAUser(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
ctx := context.Background()
|
||||
|
||||
first, err := inv.MintBusPassword(ctx, aBusUser("shop"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := inv.MintBusPassword(ctx, aBusUser("shop"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first == second {
|
||||
t.Fatal("minting twice produced the same password")
|
||||
}
|
||||
users, err := inv.BusUsers(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(users) != 1 {
|
||||
t.Fatalf("%d users after two mints for one name", len(users))
|
||||
}
|
||||
hash := users["one.shop"].PasswordHash
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(second)); err != nil {
|
||||
t.Fatal("the kept hash is not the newest password's")
|
||||
}
|
||||
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(first)) == nil {
|
||||
t.Fatal("the previous password still verifies, so a rotation revoked nothing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestForgettingANodeTakesItsBusUsersWithIt(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
ctx := context.Background()
|
||||
|
||||
for _, u := range []BusUser{
|
||||
{Username: "node.one", Kind: BusNode, Node: "one"},
|
||||
aBusUser("shop"),
|
||||
{Username: "node.two", Kind: BusNode, Node: "two"},
|
||||
{Username: "controller", Kind: BusController},
|
||||
} {
|
||||
if _, err := inv.MintBusPassword(ctx, u); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := inv.ForgetBusUsersOf(ctx, "one"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
users, err := inv.BusUsers(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, still := users["node.one"]; still {
|
||||
t.Fatal("a forgotten node's host credential still works")
|
||||
}
|
||||
if _, still := users["one.shop"]; still {
|
||||
t.Fatal("a module on a forgotten node still has a credential")
|
||||
}
|
||||
// And nothing else went with it: the controller has no node, and another machine's user is
|
||||
// another machine's.
|
||||
for _, kept := range []string{"node.two", "controller"} {
|
||||
if _, ok := users[kept]; !ok {
|
||||
t.Fatalf("%s was removed with another node's users", kept)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestForgettingTheUsersOfNoNodeIsRefused(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
if err := inv.ForgetBusUsersOf(context.Background(), ""); err == nil {
|
||||
t.Fatal("forgetting the bus users of no node was allowed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestACredentialTheMeshDidNotMintIsRecordedOnceAndNotOverwritten(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
ctx := context.Background()
|
||||
|
||||
if err := inv.SeedBusUser(ctx, BusUser{Username: "controller", Kind: BusController},
|
||||
"bootstrap"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hash, known, err := inv.BusUserHash(ctx, "controller")
|
||||
if err != nil || !known {
|
||||
t.Fatalf("the credential was not recorded: %v %v", known, err)
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte("bootstrap")); err != nil {
|
||||
t.Fatalf("what was recorded does not verify the credential given: %v", err)
|
||||
}
|
||||
|
||||
// Minted since, then seeded again — which is what a restart does. The rotation must stand, or
|
||||
// every restart would put the bootstrap password back over it.
|
||||
minted, err := inv.MintBusPassword(ctx, BusUser{Username: "controller", Kind: BusController})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.SeedBusUser(ctx, BusUser{Username: "controller", Kind: BusController},
|
||||
"bootstrap"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hash, _, err = inv.BusUserHash(ctx, "controller")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(minted)); err != nil {
|
||||
t.Fatal("a restart put the bootstrap credential back over a rotated one")
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
@@ -17,147 +16,19 @@ func aBusUser(module string) BusUser {
|
||||
// The plaintext comes back once and the store keeps only a hash that verifies against it. **A
|
||||
// credential recoverable from the mesh's store is one whose blast radius is the store's**, so what
|
||||
// is asserted is that the password is not in there.
|
||||
func TestABusPasswordIsReturnedOnceAndOnlyItsHashIsKept(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
ctx := context.Background()
|
||||
|
||||
password, err := inv.MintBusPassword(ctx, aBusUser("shop"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if password == "" {
|
||||
t.Fatal("no password came back, so nothing can be sealed to the module")
|
||||
}
|
||||
|
||||
hash, known, err := inv.BusUserHash(ctx, "one.shop")
|
||||
if err != nil || !known {
|
||||
t.Fatalf("the user was not recorded: %v %v", known, err)
|
||||
}
|
||||
if hash == password {
|
||||
t.Fatal("the store holds the password itself")
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)); err != nil {
|
||||
t.Fatalf("the recorded hash does not verify the password it was made from: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Minting again replaces what was there rather than failing or adding a second row: that is a
|
||||
// rotation, and the old credential stops working at the next composition.
|
||||
func TestMintingAgainRotatesRatherThanAddsAUser(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
ctx := context.Background()
|
||||
|
||||
first, err := inv.MintBusPassword(ctx, aBusUser("shop"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := inv.MintBusPassword(ctx, aBusUser("shop"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if first == second {
|
||||
t.Fatal("minting twice produced the same password")
|
||||
}
|
||||
users, err := inv.BusUsers(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(users) != 1 {
|
||||
t.Fatalf("%d users after two mints for one name", len(users))
|
||||
}
|
||||
hash := users["one.shop"].PasswordHash
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(second)); err != nil {
|
||||
t.Fatal("the kept hash is not the newest password's")
|
||||
}
|
||||
if bcrypt.CompareHashAndPassword([]byte(hash), []byte(first)) == nil {
|
||||
t.Fatal("the previous password still verifies, so a rotation revoked nothing")
|
||||
}
|
||||
}
|
||||
|
||||
// Forgetting a node takes every credential that belonged to it — its host's and every module
|
||||
// assigned to it. What a forgotten node leaves behind otherwise is a working set of credentials for
|
||||
// a machine the mesh no longer knows.
|
||||
func TestForgettingANodeTakesItsBusUsersWithIt(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
ctx := context.Background()
|
||||
|
||||
for _, u := range []BusUser{
|
||||
{Username: "node.one", Kind: BusNode, Node: "one"},
|
||||
aBusUser("shop"),
|
||||
{Username: "node.two", Kind: BusNode, Node: "two"},
|
||||
{Username: "controller", Kind: BusController},
|
||||
} {
|
||||
if _, err := inv.MintBusPassword(ctx, u); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := inv.ForgetBusUsersOf(ctx, "one"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
users, err := inv.BusUsers(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, still := users["node.one"]; still {
|
||||
t.Fatal("a forgotten node's host credential still works")
|
||||
}
|
||||
if _, still := users["one.shop"]; still {
|
||||
t.Fatal("a module on a forgotten node still has a credential")
|
||||
}
|
||||
// And nothing else went with it: the controller has no node, and another machine's user is
|
||||
// another machine's.
|
||||
for _, kept := range []string{"node.two", "controller"} {
|
||||
if _, ok := users[kept]; !ok {
|
||||
t.Fatalf("%s was removed with another node's users", kept)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forgetting the users of no node would forget every user that has none — the controller and every
|
||||
// person — so it is refused rather than run.
|
||||
func TestForgettingTheUsersOfNoNodeIsRefused(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
if err := inv.ForgetBusUsersOf(context.Background(), ""); err == nil {
|
||||
t.Fatal("forgetting the bus users of no node was allowed")
|
||||
}
|
||||
}
|
||||
|
||||
// The controller's own user is created by the installer, so the mesh has to be able to record a
|
||||
// credential it did not mint — or the first composition leaves the writer out of the file it writes.
|
||||
func TestACredentialTheMeshDidNotMintIsRecordedOnceAndNotOverwritten(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
ctx := context.Background()
|
||||
|
||||
if err := inv.SeedBusUser(ctx, BusUser{Username: "controller", Kind: BusController},
|
||||
"bootstrap"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hash, known, err := inv.BusUserHash(ctx, "controller")
|
||||
if err != nil || !known {
|
||||
t.Fatalf("the credential was not recorded: %v %v", known, err)
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte("bootstrap")); err != nil {
|
||||
t.Fatalf("what was recorded does not verify the credential given: %v", err)
|
||||
}
|
||||
|
||||
// Minted since, then seeded again — which is what a restart does. The rotation must stand, or
|
||||
// every restart would put the bootstrap password back over it.
|
||||
minted, err := inv.MintBusPassword(ctx, BusUser{Username: "controller", Kind: BusController})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.SeedBusUser(ctx, BusUser{Username: "controller", Kind: BusController},
|
||||
"bootstrap"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hash, _, err = inv.BusUserHash(ctx, "controller")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(minted)); err != nil {
|
||||
t.Fatal("a restart put the bootstrap credential back over a rotated one")
|
||||
}
|
||||
}
|
||||
|
||||
// Outside a test a bus password is hashed at bcrypt's default cost: only ForTest lowers it (novox/hq issue 591).
|
||||
func TestABusPasswordIsHashedAtTheDefaultCostOutsideATest(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAReportOfABusyContainerIsKeptAndReadBack(t *testing.T) {
|
||||
inv, node := aNodeWithModules(t)
|
||||
ctx := t.Context()
|
||||
record, err := inv.NodeByName(ctx, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
since := time.Date(2026, 10, 13, 1, 0, 0, 0, time.UTC)
|
||||
waiting := []BusyWait{{ID: "build-agent.server", Container: "mesh-build-agent", State: BusyWaiting,
|
||||
Since: since, Until: since.Add(15 * time.Minute), Said: "busy: a check"}}
|
||||
keep := func(busy []BusyWait) bool {
|
||||
t.Helper()
|
||||
news, err := inv.RecordOrderedDoing(ctx, record.ID, Doing{Outcome: OutcomeApplied, Declared: "d1", Applied: 1,
|
||||
Busy: busy}, ReportOrder{Epoch: 1, Sequence: 1}, func(ReportOrder) bool { return false })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return news
|
||||
}
|
||||
read := func() []BusyWait {
|
||||
t.Helper()
|
||||
reports, err := inv.LastReports(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, r := range reports {
|
||||
if r.Node == node {
|
||||
return r.Busy
|
||||
}
|
||||
}
|
||||
t.Fatal("the machine's report is not read back")
|
||||
return nil
|
||||
}
|
||||
keep(nil)
|
||||
if got := read(); len(got) != 0 {
|
||||
t.Fatalf("a report with nothing busy reads back %+v", got)
|
||||
}
|
||||
if !keep(waiting) {
|
||||
t.Error("a container starting to wait is not news")
|
||||
}
|
||||
got := read()
|
||||
if b, ok := WaitingFor(got, "build-agent"); !ok || !b.Since.Equal(since) || b.Said != "busy: a check" {
|
||||
t.Fatalf("the wait is not read back: %+v", got)
|
||||
}
|
||||
again := []BusyWait{waiting[0]}
|
||||
again[0].Said = "busy: a check, a minute on"
|
||||
if keep(again) {
|
||||
t.Error("a wait whose output changed is news")
|
||||
}
|
||||
if !keep([]BusyWait{{ID: "build-agent.server", Container: "mesh-build-agent", State: "replaced-when-idle",
|
||||
Since: since, Until: since.Add(15 * time.Minute)}}) {
|
||||
t.Error("a container replaced after it waited is not news")
|
||||
}
|
||||
if _, ok := WaitingFor(read(), "build-agent"); ok {
|
||||
t.Error("a container replaced still reads as waiting")
|
||||
}
|
||||
}
|
||||
@@ -83,63 +83,6 @@ func TestABusyContainerIsItsModules(t *testing.T) {
|
||||
|
||||
// A machine's report of a busy container is kept with its last account and read back beside it, and its
|
||||
// starting and its end are news; its output alone is not (novox/hq ADR 0335). Needs the store.
|
||||
func TestAReportOfABusyContainerIsKeptAndReadBack(t *testing.T) {
|
||||
inv, node := aNodeWithModules(t)
|
||||
ctx := t.Context()
|
||||
record, err := inv.NodeByName(ctx, node)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
since := time.Date(2026, 10, 13, 1, 0, 0, 0, time.UTC)
|
||||
waiting := []BusyWait{{ID: "build-agent.server", Container: "mesh-build-agent", State: BusyWaiting,
|
||||
Since: since, Until: since.Add(15 * time.Minute), Said: "busy: a check"}}
|
||||
keep := func(busy []BusyWait) bool {
|
||||
t.Helper()
|
||||
news, err := inv.RecordOrderedDoing(ctx, record.ID, Doing{Outcome: OutcomeApplied, Declared: "d1", Applied: 1,
|
||||
Busy: busy}, ReportOrder{Epoch: 1, Sequence: 1}, func(ReportOrder) bool { return false })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return news
|
||||
}
|
||||
read := func() []BusyWait {
|
||||
t.Helper()
|
||||
reports, err := inv.LastReports(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, r := range reports {
|
||||
if r.Node == node {
|
||||
return r.Busy
|
||||
}
|
||||
}
|
||||
t.Fatal("the machine's report is not read back")
|
||||
return nil
|
||||
}
|
||||
keep(nil)
|
||||
if got := read(); len(got) != 0 {
|
||||
t.Fatalf("a report with nothing busy reads back %+v", got)
|
||||
}
|
||||
if !keep(waiting) {
|
||||
t.Error("a container starting to wait is not news")
|
||||
}
|
||||
got := read()
|
||||
if b, ok := WaitingFor(got, "build-agent"); !ok || !b.Since.Equal(since) || b.Said != "busy: a check" {
|
||||
t.Fatalf("the wait is not read back: %+v", got)
|
||||
}
|
||||
again := []BusyWait{waiting[0]}
|
||||
again[0].Said = "busy: a check, a minute on"
|
||||
if keep(again) {
|
||||
t.Error("a wait whose output changed is news")
|
||||
}
|
||||
if !keep([]BusyWait{{ID: "build-agent.server", Container: "mesh-build-agent", State: "replaced-when-idle",
|
||||
Since: since, Until: since.Add(15 * time.Minute)}}) {
|
||||
t.Error("a container replaced after it waited is not news")
|
||||
}
|
||||
if _, ok := WaitingFor(read(), "build-agent"); ok {
|
||||
t.Error("a container replaced still reads as waiting")
|
||||
}
|
||||
}
|
||||
|
||||
// A machine of the rest that answered and waits for a busy container is never read as silent, its wait is kept
|
||||
// out of its apply time, and once replaced its report is the end of the wait (novox/hq ADR 0335).
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
// A carried peer is nameable (novox/hq issue 112): the operator states which machine a carried
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
func TestAWrongPasswordIsAnAnswerNotAnOutage(t *testing.T) {
|
||||
dsn := os.Getenv("MESH_TEST_POSTGRES")
|
||||
if dsn == "" {
|
||||
t.Skip("MESH_TEST_POSTGRES is not set")
|
||||
}
|
||||
wrong := strings.Replace(dsn, "postgres:check@", "postgres:not-the-password@", 1)
|
||||
if wrong == dsn {
|
||||
t.Skip("the test store's address does not carry the expected credential")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_, err := pgconn.Connect(ctx, wrong)
|
||||
if err == nil {
|
||||
t.Fatal("a wrong password connected")
|
||||
}
|
||||
if Unreachable(err) {
|
||||
t.Fatalf("a wrong password was taken for an outage and would be retried for ever: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -55,22 +53,3 @@ func TestAStoreThatRefusesTheConnectionIsUnreachable(t *testing.T) {
|
||||
|
||||
// A connection the store refused on a wrong password is an answer, even though it arrives as a
|
||||
// failed connection — asked again for ever, it would hold every message behind it.
|
||||
func TestAWrongPasswordIsAnAnswerNotAnOutage(t *testing.T) {
|
||||
dsn := os.Getenv("MESH_TEST_POSTGRES")
|
||||
if dsn == "" {
|
||||
t.Skip("MESH_TEST_POSTGRES is not set")
|
||||
}
|
||||
wrong := strings.Replace(dsn, "postgres:check@", "postgres:not-the-password@", 1)
|
||||
if wrong == dsn {
|
||||
t.Skip("the test store's address does not carry the expected credential")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
_, err := pgconn.Connect(ctx, wrong)
|
||||
if err == nil {
|
||||
t.Fatal("a wrong password connected")
|
||||
}
|
||||
if Unreachable(err) {
|
||||
t.Fatalf("a wrong password was taken for an outage and would be retried for ever: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
func TestAModuleInTheConfigFormIsMirroredWithItsWholeDeclaration(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
var m catalogue.Manifest
|
||||
if err := json.Unmarshal([]byte(`{"module": "proxy", "version": "1", "config": {"port": {"type": "number",
|
||||
"description": "the port it listens on, a TCP port number", "default": 8080, "minimum": 1,
|
||||
"maximum": 65535, "enum": [8080, 8443], "predefined": "http.port"}}}`), &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RegisterModule(t.Context(), m, Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
values, err := inv.DeclaredConfigMirror(t.Context())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got DeclaredConfigMirrored
|
||||
if err := json.Unmarshal(values["proxy"], &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
port, has := got.Settings["port"]
|
||||
if !has || port.Type != catalogue.ConfigNumber || port.Description == "" || string(port.Default) != "8080" ||
|
||||
port.Minimum == nil || *port.Minimum != 1 || port.Maximum == nil || *port.Maximum != 65535 ||
|
||||
string(port.Enum) != "[8080,8443]" || port.Predefined != "http.port" {
|
||||
t.Errorf("a module in the config form is mirrored as %s", values["proxy"])
|
||||
}
|
||||
}
|
||||
@@ -85,29 +85,3 @@ func TestADeclarationUsingEveryFieldRoundTripsThroughTheMirror(t *testing.T) {
|
||||
// **A module moved to the config form is mirrored from it** (ADR 0348 §1): the store keeps its manifest in the
|
||||
// form it was written in, under `config`, and the declared-config mirror reads its declarations from there with
|
||||
// the config form's fields.
|
||||
func TestAModuleInTheConfigFormIsMirroredWithItsWholeDeclaration(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
var m catalogue.Manifest
|
||||
if err := json.Unmarshal([]byte(`{"module": "proxy", "version": "1", "config": {"port": {"type": "number",
|
||||
"description": "the port it listens on, a TCP port number", "default": 8080, "minimum": 1,
|
||||
"maximum": 65535, "enum": [8080, 8443], "predefined": "http.port"}}}`), &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RegisterModule(t.Context(), m, Source{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
values, err := inv.DeclaredConfigMirror(t.Context())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got DeclaredConfigMirrored
|
||||
if err := json.Unmarshal(values["proxy"], &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
port, has := got.Settings["port"]
|
||||
if !has || port.Type != catalogue.ConfigNumber || port.Description == "" || string(port.Default) != "8080" ||
|
||||
port.Minimum == nil || *port.Minimum != 1 || port.Maximum == nil || *port.Maximum != 65535 ||
|
||||
string(port.Enum) != "[8080,8443]" || port.Predefined != "http.port" {
|
||||
t.Errorf("a module in the config form is mirrored as %s", values["proxy"])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//go:build integration
|
||||
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
func TestWhatRunsIsTheRegisteredBuildNotANewerOnePutBack(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
ctx := t.Context()
|
||||
manifestOf := func(image string) catalogue.Manifest {
|
||||
return catalogue.Manifest{Module: "one", Version: "1", Resources: []map[string]any{
|
||||
{"id": "app", "type": "container", "name": "one", "image": image}}}
|
||||
}
|
||||
record := func(id, image string, asked time.Time) Build {
|
||||
t.Helper()
|
||||
raw, _ := json.Marshal(manifestOf(image))
|
||||
b := Build{ID: id, Module: "one", Commit: id + "aaaaaaaa", Repository: "novox/one", Manifest: raw,
|
||||
Asked: asked, At: asked, Made: []Artifact{{Name: "one", Kind: "image", Reference: image}}}
|
||||
if err := inv.RecordBuild(ctx, b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RegisterModule(ctx, manifestOf(image), Source{Repository: "novox/one", BuiltFrom: b.Commit,
|
||||
Asked: asked}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
x := "store/one@sha256:" + "1111111111111111111111111111111111111111111111111111111111111111"
|
||||
y := "store/one@sha256:" + "2222222222222222222222222222222222222222222222222222222222222222"
|
||||
before := record("x", x, time.Now().Add(-2*time.Hour))
|
||||
failed := record("y", y, time.Now().Add(-time.Minute))
|
||||
if err := inv.RecordGate(ctx, GateVerdict{Build: failed.ID, Module: "one", Commit: failed.Commit,
|
||||
Machines: []string{"anchor"}, Verdict: GateFailed}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RestoreModule(ctx, before); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
held, err := inv.Held(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
running, err := inv.RegisteredHeld(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if running["one/one"] == y {
|
||||
t.Fatalf("what runs is read as the build whose gate failed (%s); Held says %s", y, held["one/one"])
|
||||
}
|
||||
if got := running["one/one"]; got != "" && got != x {
|
||||
t.Fatalf("what runs is read as %s, want the build put back %s or nothing", got, x)
|
||||
}
|
||||
}
|
||||
@@ -1,63 +1,12 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
// TestWhatRunsIsTheRegisteredBuildNotANewerOnePutBack is novox/hq issue 721's review: the newest build of a module
|
||||
// failed its gate and the mesh put the build before it back. Held names the newest; what runs, and what a merge's build
|
||||
// is compared with, is the build put back — so a merge whose bytes equal the failed build is never said unchanged.
|
||||
func TestWhatRunsIsTheRegisteredBuildNotANewerOnePutBack(t *testing.T) {
|
||||
inv := fresh(t)
|
||||
ctx := t.Context()
|
||||
manifestOf := func(image string) catalogue.Manifest {
|
||||
return catalogue.Manifest{Module: "one", Version: "1", Resources: []map[string]any{
|
||||
{"id": "app", "type": "container", "name": "one", "image": image}}}
|
||||
}
|
||||
record := func(id, image string, asked time.Time) Build {
|
||||
t.Helper()
|
||||
raw, _ := json.Marshal(manifestOf(image))
|
||||
b := Build{ID: id, Module: "one", Commit: id + "aaaaaaaa", Repository: "novox/one", Manifest: raw,
|
||||
Asked: asked, At: asked, Made: []Artifact{{Name: "one", Kind: "image", Reference: image}}}
|
||||
if err := inv.RecordBuild(ctx, b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RegisterModule(ctx, manifestOf(image), Source{Repository: "novox/one", BuiltFrom: b.Commit,
|
||||
Asked: asked}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
x := "store/one@sha256:" + "1111111111111111111111111111111111111111111111111111111111111111"
|
||||
y := "store/one@sha256:" + "2222222222222222222222222222222222222222222222222222222222222222"
|
||||
before := record("x", x, time.Now().Add(-2*time.Hour))
|
||||
failed := record("y", y, time.Now().Add(-time.Minute))
|
||||
if err := inv.RecordGate(ctx, GateVerdict{Build: failed.ID, Module: "one", Commit: failed.Commit,
|
||||
Machines: []string{"anchor"}, Verdict: GateFailed}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := inv.RestoreModule(ctx, before); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
held, err := inv.Held(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
running, err := inv.RegisteredHeld(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if running["one/one"] == y {
|
||||
t.Fatalf("what runs is read as the build whose gate failed (%s); Held says %s", y, held["one/one"])
|
||||
}
|
||||
if got := running["one/one"]; got != "" && got != x {
|
||||
t.Fatalf("what runs is read as %s, want the build put back %s or nothing", got, x)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOnlyArtifactsTheRegisteredManifestNamesAreRunning: registeredOnly keeps a held artifact only when its digest is
|
||||
// in its module's registered manifest.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user