an image artifact may name its own build context, apart from the module's repository

route-proxy's own Dockerfile documents the shape it has always needed and
never had: 'the proxy source is not vendored here... the build context is
the mesh-controller repository root, and this Dockerfile compiles
./examples/route-proxy from it.' Nothing in the mesh could do that — the
build command clones one repository and builds every artifact from
within it, so route-proxy has never once been built through the pipeline,
consistent with it never having been assigned anywhere. Found attempting
exactly that build tonight: 'stat go.mod: file does not exist', because
the context was mesh-catalog, which does not have one.

An image artifact may now carry a context: {repository, ref}, cloned
fresh alongside the module's own tree. The recipe (Dockerfile) is still
read from the module's own directory, at the module's own commit — only
docker build's own context argument moves. Packaging and source stay
exactly as separate as route-proxy's own comment already said they were,
now for real.
This commit is contained in:
2026-09-25 17:39:27 +02:00
parent 7bf23ea052
commit 20e57c3f51
3 changed files with 153 additions and 8 deletions
+83 -3
View File
@@ -18,13 +18,19 @@ import (
// tree, that two builds of one commit produce one digest. Running docker here would test docker.
type recorded struct {
ran []string
ran []string
// dirs is the directory each entry in ran was run from, same index — so a test can ask not
// only what ran but where.
dirs []string
images map[string]string
archives map[string]string
failPush bool
// contents is what a clone of this repository lands, so the fake clone can restore the tree
// Build deliberately removes first.
contents map[string]string
// secondary is what a clone of a repository OTHER than the one under test lands, keyed by
// that repository's URL — an artifact's own build context, cloned apart from the module.
secondary map[string]map[string]string
// stamped is the modification time the clone gives every file. Set differently between two
// builds of one commit, because otherwise both land in the same second and a packer that
// carried timestamps would still produce one digest — which is a test that passes for a
@@ -35,13 +41,19 @@ type recorded struct {
func (r *recorded) run(_ context.Context, dir, name string, args ...string) (string, error) {
line := name + " " + strings.Join(args, " ")
r.ran = append(r.ran, line)
r.dirs = append(r.dirs, dir)
switch {
case name == "git" && len(args) > 0 && args[0] == "clone":
repository := args[len(args)-2]
tree := args[len(args)-1]
if err := os.MkdirAll(tree, 0o755); err != nil {
return "", err
}
for path, body := range r.contents {
lands := r.contents
if by, is := r.secondary[repository]; is {
lands = by
}
for path, body := range lands {
full := filepath.Join(tree, path)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
return "", err
@@ -59,7 +71,6 @@ func (r *recorded) run(_ context.Context, dir, name string, args ...string) (str
case name == "git" && len(args) > 0 && args[0] == "rev-parse":
return "c0ffeec0ffeec0ffeec0ffeec0ffeec0ffeec0ff\n", nil
}
_ = dir
return "", nil
}
@@ -331,3 +342,72 @@ func TestAPathThatLeavesTheRepositoryIsRefused(t *testing.T) {
}
}
}
// **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{}, 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)
}
}