A resource can name the version of the build it uses #156
@@ -94,12 +94,43 @@ func (m Manifest) Resolve(built []Built) (Manifest, error) {
|
||||
m.Module, r["id"], named)
|
||||
case ArtifactImage, ArtifactUpstream:
|
||||
filled["image"] = artifact.Reference
|
||||
// An image is not unpacked anywhere, so it has no directory to be named for its
|
||||
// version and `${version}` has nothing to mean. Refused rather than left as literal
|
||||
// text in a path, which is how it would reach a machine and be created as a directory
|
||||
// called `${version}`.
|
||||
for key, value := range filled {
|
||||
if text, isText := value.(string); isText && strings.Contains(text, versionRef) {
|
||||
return Manifest{}, fmt.Errorf(
|
||||
"%s: %v says %s in %q, and %q is an image — an image is not unpacked, so "+
|
||||
"it has no versioned place. %s is for an archive or a bundle",
|
||||
m.Module, r["id"], versionRef, key, named, versionRef)
|
||||
}
|
||||
}
|
||||
case ArtifactArchive, ArtifactBundle:
|
||||
// The same on the wire: both are bytes fetched by digest and unpacked. They differ in
|
||||
// how they were made — one packed as it stood, the other compiled first — and a
|
||||
// machine has no reason to care which.
|
||||
filled["source"] = artifact.Reference
|
||||
filled["digest"] = artifact.Digest
|
||||
// **And `${version}`, so a resource can name a place that is this build's alone**
|
||||
// (novox/hq ADR 0141, 04-ISSUES/142). A component is unpacked into a directory named
|
||||
// for its version so it can read its own version from its path — and until this,
|
||||
// nothing could compose that path: an archive named a fixed one in the manifest and
|
||||
// nothing interpolated the build into it, so nothing could ask for
|
||||
// `…/versions/<version>/` and every machine took a hand-placed fallback.
|
||||
//
|
||||
// The version is the artifact's own digest, short. Not the commit: two builds of one
|
||||
// commit are meant to be the same bytes (the toolchains are `-trimpath` for this), and
|
||||
// a content-addressed version means an unchanged build resolves to the path it already
|
||||
// had — so re-composing a declaration moves nothing, where a commit would move the
|
||||
// path of an identical binary and recreate everything that reads it.
|
||||
for key, value := range filled {
|
||||
text, isText := value.(string)
|
||||
if !isText || !strings.Contains(text, versionRef) {
|
||||
continue
|
||||
}
|
||||
filled[key] = strings.ReplaceAll(text, versionRef, versionOf(artifact.Digest))
|
||||
}
|
||||
default:
|
||||
return Manifest{}, fmt.Errorf("%s: %q is a %q, and an artifact is %q, %q, %q or %q",
|
||||
m.Module, named, artifact.Kind, ArtifactImage, ArtifactArchive, ArtifactUpstream,
|
||||
@@ -259,3 +290,28 @@ func compilesToABinary(language string) bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// versionRef is how a resource names the version of the artifact it uses: ${version}.
|
||||
//
|
||||
// No artifact name in it, because the resource already says which artifact it is for — a second
|
||||
// name would be a second thing to keep in step with the first.
|
||||
const versionRef = "${version}"
|
||||
|
||||
// versionOf is an artifact's version as a path names it: its digest, short.
|
||||
//
|
||||
// **Content-addressed on purpose.** The alternative is the commit a build came from, and two builds
|
||||
// of one commit are meant to produce the same bytes — every toolchain here is `-trimpath` for that
|
||||
// reason. A commit-named path would move for an identical binary, and everything reading that path
|
||||
// would be recreated for a change that is not one. A digest-named path moves exactly when the bytes
|
||||
// do.
|
||||
//
|
||||
// Twelve hex characters: enough that two of this mesh's builds will not collide, short enough to
|
||||
// read in a path and in a journal line. The `sha256:` prefix goes, because a directory name carrying
|
||||
// a colon is a directory name people quote wrong.
|
||||
func versionOf(digest string) string {
|
||||
hex := strings.TrimPrefix(strings.TrimSpace(digest), "sha256:")
|
||||
if len(hex) > 12 {
|
||||
return hex[:12]
|
||||
}
|
||||
return hex
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A component is unpacked into a directory named for its version, so it can read its own version from
|
||||
// its path (novox/hq ADR 0141, 0142). Until this, nothing could compose that path: an archive named a
|
||||
// fixed one and nothing interpolated the build into it, so nothing could ask for
|
||||
// `…/versions/<version>/` and every machine took a hand-placed fallback (04-ISSUES/142).
|
||||
|
||||
const aDigest = "sha256:ad62528c47c7b4a71cf814473f5de52a061348ce9521f707b0171a10fa6b247f"
|
||||
|
||||
func TestAnArchivePathCanNameTheBuildsOwnVersion(t *testing.T) {
|
||||
m := Manifest{
|
||||
Module: "mesh-host",
|
||||
Build: &Build{Artifacts: []Artifact{{Name: "host-arch", Kind: ArtifactBundle, Language: "go", System: "arch"}}},
|
||||
Resources: []map[string]any{{
|
||||
"id": "next", "type": "archive", "artifact": "host-arch",
|
||||
"path": "/usr/lib/nox-mesh-host/versions/${version}",
|
||||
}},
|
||||
}
|
||||
got, err := m.Resolve([]Built{{Name: "host-arch", Kind: ArtifactBundle,
|
||||
Reference: "artifact-store://mesh-host/host-arch", Digest: aDigest}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path, _ := got.Resources[0]["path"].(string)
|
||||
if strings.Contains(path, "${version}") {
|
||||
t.Fatalf("the version was not resolved: %q", path)
|
||||
}
|
||||
if path != "/usr/lib/nox-mesh-host/versions/ad62528c47c7" {
|
||||
t.Fatalf("the path resolved to %q", path)
|
||||
}
|
||||
// The artifact key goes, as it does for every resolved resource: it is a build-time word and the
|
||||
// host has never heard of it.
|
||||
if _, still := got.Resources[0]["artifact"]; still {
|
||||
t.Fatal("the artifact key survived resolution")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheVersionIsTheDigestSoAnUnchangedBuildKeepsItsPath(t *testing.T) {
|
||||
// The alternative is the commit, and two builds of one commit are meant to be the same bytes —
|
||||
// every toolchain here is -trimpath for that reason. A commit-named path would move for an
|
||||
// identical binary and recreate everything reading it.
|
||||
first := versionOf(aDigest)
|
||||
again := versionOf(aDigest)
|
||||
if first != again || first == "" {
|
||||
t.Fatalf("the same bytes produced %q and %q", first, again)
|
||||
}
|
||||
if other := versionOf("sha256:" + strings.Repeat("b", 64)); other == first {
|
||||
t.Fatal("different bytes produced the same version")
|
||||
}
|
||||
// A path is read by people and quoted by shells.
|
||||
if strings.ContainsAny(first, ":/ ") {
|
||||
t.Fatalf("the version is not safe in a path: %q", first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnImageIsRefusedAVersionedPlace(t *testing.T) {
|
||||
// An image is not unpacked, so it has no directory to be named for its version. Left as literal
|
||||
// text it would reach a machine and be created as a directory called ${version}.
|
||||
m := Manifest{
|
||||
Module: "something",
|
||||
Build: &Build{Artifacts: []Artifact{{Name: "server", Kind: ArtifactImage, From: "Dockerfile"}}},
|
||||
Resources: []map[string]any{{
|
||||
"id": "where", "type": "directory", "artifact": "server",
|
||||
"path": "/var/lib/something/${version}",
|
||||
}},
|
||||
}
|
||||
_, err := m.Resolve([]Built{{Name: "server", Kind: ArtifactImage, Reference: "registry/x@" + aDigest}})
|
||||
if err == nil {
|
||||
t.Fatal("an image was given a versioned place")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "not unpacked") {
|
||||
t.Fatalf("the refusal does not say why: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAResourceWithoutAVersionReferenceIsUntouched(t *testing.T) {
|
||||
m := Manifest{
|
||||
Module: "mesh-host",
|
||||
Build: &Build{Artifacts: []Artifact{{Name: "host-arch", Kind: ArtifactBundle, Language: "go", System: "arch"}}},
|
||||
Resources: []map[string]any{{
|
||||
"id": "next", "type": "archive", "artifact": "host-arch", "path": "/usr/lib/fixed",
|
||||
}},
|
||||
}
|
||||
got, err := m.Resolve([]Built{{Name: "host-arch", Kind: ArtifactBundle,
|
||||
Reference: "artifact-store://mesh-host/host-arch", Digest: aDigest}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if path, _ := got.Resources[0]["path"].(string); path != "/usr/lib/fixed" {
|
||||
t.Fatalf("a path naming no version became %q", path)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user