package builder import ( "context" "fmt" "strings" ) // Local is what a build publishes into when there is nowhere to publish yet. // // **This exists for exactly one moment: raising a mesh** (novox/hq ADR 0073). The installer builds // the control plane on the machine that is about to run it, and at that moment there is no registry // — the registry is installed afterwards, by the control plane this build produces. So the artifact // stays where the build left it: in the machine's own container runtime. // // That is not a weaker kind of pinning. An image held locally is named by the digest of its own // configuration, which is content-addressed and unforgeable and requires nothing to have served it // — the same identity the installer has always used for the image it carried. What changes when a // registry exists is not that the artifact becomes exact, but that something other than this // machine can fetch it. // // It is deliberately unable to publish an archive. An archive has no local identity to fall back // on: it is bytes that only mean something once something serves them at a URL. A build that // produces one before there is anywhere to put it has produced nothing usable, and saying so is // better than returning a path on a disk that no other machine can read. type Local struct { // Run is how docker is invoked, so a test does not need one. Run Runner } // PublishImage leaves the image where the build put it, and names it by its own configuration. // // The local tag is not returned: a tag is a name somebody can move, and every other reference in a // resolved manifest is exact. The digest is read back from the runtime rather than computed, for // the same reason the registry publisher reads it back from the registry — what matters is what // will be served for that reference, and only the thing serving it can say. func (l Local) PublishImage(ctx context.Context, localTag, repository string) (string, error) { out, err := l.Run(ctx, "", "docker", "image", "inspect", "--format", "{{.Id}}", localTag) if err != nil { return "", fmt.Errorf("cannot read back the image just built as %s: %w", localTag, err) } id := strings.TrimSpace(out) if !strings.HasPrefix(id, "sha256:") || len(id) != len("sha256:")+64 { // Refused rather than passed on. A bundle naming an image by anything a person could move // is refused by the machine applying it, and a value that is not an identity would fail // there instead — one step further from the thing that could explain it. return "", fmt.Errorf( "the container runtime named the image just built %q, which is not an image id: "+ "sha256 and sixty-four hex characters", id) } return id, nil } // PublishArchive refuses, and says why rather than inventing somewhere to put it. func (l Local) PublishArchive(ctx context.Context, repository string, body []byte, digest string) (string, error) { return "", fmt.Errorf( "%s declares an archive, and this build has nowhere to publish one. An image can stay in "+ "the machine's own runtime and still be named exactly; an archive is bytes that mean "+ "nothing until something serves them. Build this once the mesh has a registry", repository) }