A build declares the vendor image it stands on, and a recipe fetches nothing undeclared
build.on takes {arg, image@sha256:…} beside {arg, module, artifact}: the image is
copied into the mesh's registry before the build (ADR 0096) and the recipe reads the
copy from the argument. A FROM or COPY --from naming a registry image the manifest
did not declare is refused before the build, naming it and the remedy; stages,
declared arguments and scratch are not fetches (novox/hq 04-ISSUES/064, ADR 0097).
This commit is contained in:
+127
-2
@@ -14,6 +14,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -149,7 +150,20 @@ func Build(ctx context.Context, run Runner, publish Publisher,
|
||||
// What this module said it stands on, answered with what this mesh actually holds. Done
|
||||
// before anything is built, so a missing base is refused in front of the person who can
|
||||
// fix it rather than inside a build that stops on its own first line.
|
||||
args, err := standingOn(manifest, held)
|
||||
// An image published elsewhere that the build stands on is copied into the mesh's own
|
||||
// registry first, like an upstream artifact (ADR 0096), and the recipe is handed the copy.
|
||||
// Genesis has nowhere to copy to and pulls it into this machine's store instead.
|
||||
mirror := func(ctx context.Context, from, repository string) (string, error) {
|
||||
if m, can := publish.(Mirrorer); can {
|
||||
say("bases", "copying %s into the mesh's registry", from)
|
||||
return m.MirrorImage(ctx, from, repository)
|
||||
}
|
||||
if _, err := run(ctx, tree, "docker", "pull", from); err != nil {
|
||||
return "", fmt.Errorf("cannot fetch %s: %w", from, err)
|
||||
}
|
||||
return from, nil
|
||||
}
|
||||
args, err := standingOn(ctx, manifest, held, mirror)
|
||||
if err != nil {
|
||||
say("bases", "UNMET: %v", err)
|
||||
return Result{}, err
|
||||
@@ -358,6 +372,29 @@ func one(ctx context.Context, run Runner, publish Publisher,
|
||||
local := fmt.Sprintf("%s-%s:%s", module, a.Name, short(commit))
|
||||
// The bases this module named, resolved to what this mesh holds. A recipe reads them as
|
||||
// build arguments, so a module says which module it stands on and never which copy.
|
||||
// **A recipe fetches nothing the manifest did not declare** (novox/hq 04-ISSUES/064). A FROM
|
||||
// or a COPY --from naming a registry image that is not a declared base is a build that
|
||||
// reaches a public registry on its own — and works when that registry answers, which is
|
||||
// sometimes. Refused here, in front of the person who can declare it, not inside a build
|
||||
// that fails with "pull access denied" for a reason that is not the mesh's.
|
||||
recipe, err := os.ReadFile(filepath.Join(tree, a.From))
|
||||
if err != nil {
|
||||
return catalogue.Built{}, fmt.Errorf("%s: cannot read the recipe %s: %w", module, a.From, err)
|
||||
}
|
||||
declared := map[string]bool{}
|
||||
for i := 0; i+1 < len(args); i += 2 {
|
||||
if args[i] == "--build-arg" {
|
||||
declared[strings.SplitN(args[i+1], "=", 2)[0]] = true
|
||||
}
|
||||
}
|
||||
if fetches := undeclaredFetches(string(recipe), declared); len(fetches) > 0 {
|
||||
return catalogue.Built{}, fmt.Errorf(
|
||||
"%s: the recipe %s fetches %s, which the manifest does not declare. A build "+
|
||||
"reaching a public registry on its own works only when that registry answers; "+
|
||||
"declare it under build.on as {\"arg\": \"<NAME>\", \"image\": \"<image>@sha256:…\"} "+
|
||||
"and read it from that argument (novox/hq ADR 0097)",
|
||||
module, a.From, strings.Join(fetches, ", "))
|
||||
}
|
||||
invocation := append([]string{"build", "-f", a.From, "-t", local}, args...)
|
||||
if a.Target != "" {
|
||||
invocation = append(invocation, "--target", a.Target)
|
||||
@@ -578,7 +615,8 @@ var _ io.Writer = (*stringWriter)(nil)
|
||||
// one a container runtime produces when a recipe's first line refers to an image nobody has.
|
||||
//
|
||||
// The order is fixed so two builds of one commit invoke the same command.
|
||||
func standingOn(manifest catalogue.Manifest, held map[string]string) ([]string, error) {
|
||||
func standingOn(ctx context.Context, manifest catalogue.Manifest, held map[string]string,
|
||||
mirror func(ctx context.Context, from, repository string) (string, error)) ([]string, error) {
|
||||
if manifest.Build == nil || len(manifest.Build.On) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -587,6 +625,27 @@ func standingOn(manifest catalogue.Manifest, held map[string]string) ([]string,
|
||||
|
||||
var args []string
|
||||
for _, base := range on {
|
||||
if base.Image != "" {
|
||||
// A vendor's image, declared (novox/hq 04-ISSUES/064, ADR 0097). Pinned, because a tag
|
||||
// is what somebody else can move; copied into the mesh's registry, because a build
|
||||
// that reaches a public registry on its own is a build that works sometimes.
|
||||
if base.Arg == "" || base.Module != "" || base.Artifact != "" {
|
||||
return nil, fmt.Errorf(
|
||||
"%s stands on the image %s, and a base is either a module's artifact or an "+
|
||||
"image — never both — read from one build argument", manifest.Module, base.Image)
|
||||
}
|
||||
if !strings.Contains(base.Image, "@sha256:") {
|
||||
return nil, fmt.Errorf(
|
||||
"%s stands on the image %q, which is not pinned by digest. A tag is what "+
|
||||
"somebody else can move; name it as <image>@sha256:…", manifest.Module, base.Image)
|
||||
}
|
||||
reference, err := mirror(ctx, base.Image, manifest.Module+"/on-"+strings.ToLower(base.Arg))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s stands on %s: %w", manifest.Module, base.Image, err)
|
||||
}
|
||||
args = append(args, "--build-arg", base.Arg+"="+reference)
|
||||
continue
|
||||
}
|
||||
if base.Arg == "" || base.Module == "" || base.Artifact == "" {
|
||||
return nil, fmt.Errorf(
|
||||
"%s says its build stands on something, and does not say all of what: a base "+
|
||||
@@ -727,3 +786,69 @@ func sourcesFor(entrypoints []string, out string) []string {
|
||||
|
||||
func timeNow() time.Time { return time.Now() }
|
||||
func since(t time.Time) string { return time.Since(t).Round(time.Millisecond).String() }
|
||||
|
||||
// undeclaredFetches is every image a recipe reaches for that is neither a declared build argument
|
||||
// nor one of its own stages nor `scratch`: a `FROM` or a `COPY --from` naming somebody else's
|
||||
// registry directly.
|
||||
func undeclaredFetches(recipe string, declared map[string]bool) []string {
|
||||
stages := map[string]bool{}
|
||||
var out []string
|
||||
seen := map[string]bool{}
|
||||
note := func(ref string) {
|
||||
ref = strings.TrimSpace(ref)
|
||||
switch {
|
||||
case ref == "" || ref == "scratch" || stages[strings.ToLower(ref)]:
|
||||
return
|
||||
case strings.HasPrefix(ref, "$"):
|
||||
name := strings.Trim(strings.TrimPrefix(ref, "$"), "{}")
|
||||
if cut := strings.IndexAny(name, ":-"); cut >= 0 {
|
||||
name = name[:cut]
|
||||
}
|
||||
if !declared[name] {
|
||||
if !seen[ref] {
|
||||
seen[ref] = true
|
||||
out = append(out, ref+" (a build argument the manifest does not declare)")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
// A stage referenced by number (COPY --from=0) is its own recipe's.
|
||||
if _, err := strconv.Atoi(ref); err == nil {
|
||||
return
|
||||
}
|
||||
if !seen[ref] {
|
||||
seen[ref] = true
|
||||
out = append(out, ref)
|
||||
}
|
||||
}
|
||||
for _, raw := range strings.Split(recipe, "\n") {
|
||||
line := strings.TrimSpace(raw)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
switch strings.ToUpper(fields[0]) {
|
||||
case "FROM":
|
||||
// FROM [--platform=…] <ref> [AS <name>]
|
||||
var ref string
|
||||
for i := 1; i < len(fields); i++ {
|
||||
if strings.HasPrefix(fields[i], "--") {
|
||||
continue
|
||||
}
|
||||
ref = fields[i]
|
||||
if i+2 < len(fields) && strings.EqualFold(fields[i+1], "AS") {
|
||||
stages[strings.ToLower(fields[i+2])] = true
|
||||
}
|
||||
break
|
||||
}
|
||||
note(ref)
|
||||
case "COPY", "ADD":
|
||||
for _, f := range fields[1:] {
|
||||
if strings.HasPrefix(f, "--from=") {
|
||||
note(strings.TrimPrefix(f, "--from="))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user