package catalogue import ( "fmt" "sort" "strings" ) // Turning a manifest that names artifacts into one that names digests. // // **Two documents, deliberately.** The manifest in a repository says *this resource uses the // archive called `config`*; the manifest the mesh holds says *this resource is sha256:…*. A digest // is not knowable until something is built, so a repository carrying one would be a repository // whose file is wrong the moment anybody edits anything — and the mesh would be pinning a value // nobody could have checked. // // So the built manifest is **derived**, and the record of which commit it was derived from is what // makes "is this current?" answerable without building (novox/hq ADR 0009). // Built is one artifact after it exists: where it is and what it hashes to. type Built struct { // Name is what the manifest called it. Name string // Kind is "image" or "archive". Kind string // Reference is what a machine uses to get it — an image reference for an image, a URL for an // archive. Both already carry the digest for an image; an archive carries it separately. Reference string // Digest is "sha256:", for an archive. An image reference already ends in one. Digest string } // Resolve fills a manifest's resources in from what was built. // // Every resource naming an artifact is rewritten to name the thing itself, and the `artifact` key // is removed — because it is a build-time word and the host has never heard of it. A resource // naming an artifact nothing produced is refused: it would otherwise reach a machine with an // empty image or an unpinned archive, which is the shape of failure that looks like success. func (m Manifest) Resolve(built []Built) (Manifest, error) { if m.Build == nil && len(built) == 0 { return m, nil } by := map[string]Built{} for _, b := range built { by[b.Name] = b } // Declared and not produced is a build that did not do what the manifest asked, and saying so // here beats a machine reporting it later. var missing []string if m.Build != nil { for _, a := range m.Build.Artifacts { if _, ok := by[a.Name]; !ok { missing = append(missing, a.Name) } } } if len(missing) > 0 { sort.Strings(missing) return Manifest{}, fmt.Errorf( "%s says it builds %s and the build did not produce %s — the build did not do what "+ "the manifest asked, which is a different fault from a resource asking for the "+ "wrong thing", m.Module, strings.Join(missing, " and "), oneOrOther(len(missing))) } out := m out.Build = nil out.Resources = nil for _, r := range m.Resources { named, _ := r["artifact"].(string) if named == "" { out.Resources = append(out.Resources, r) continue } artifact, ok := by[named] if !ok { return Manifest{}, fmt.Errorf( "%s: %v uses the artifact %q, and this module builds no such thing", m.Module, r["id"], named) } filled := map[string]any{} for k, v := range r { filled[k] = v } delete(filled, "artifact") switch artifact.Kind { case ArtifactImage: filled["image"] = artifact.Reference case ArtifactArchive: filled["source"] = artifact.Reference filled["digest"] = artifact.Digest default: return Manifest{}, fmt.Errorf("%s: %q is a %q, and an artifact is %q or %q", m.Module, named, artifact.Kind, ArtifactImage, ArtifactArchive) } out.Resources = append(out.Resources, filled) } return out, nil } // checkBuild is the manifest's own account of what it builds. func (b *Build) problems(module string) []string { if b == nil { return nil } var problems []string seen := map[string]bool{} for _, a := range b.Artifacts { if a.Name == "" { problems = append(problems, module+" builds an artifact with no name") continue } if seen[a.Name] { problems = append(problems, fmt.Sprintf( "%s builds two artifacts called %q, and a resource naming it could mean either", module, a.Name)) } seen[a.Name] = true if a.Kind != ArtifactImage && a.Kind != ArtifactArchive { problems = append(problems, fmt.Sprintf("%s: %q is a %q, and an artifact is %q or %q", module, a.Name, a.Kind, ArtifactImage, ArtifactArchive)) } if a.From == "" { problems = append(problems, fmt.Sprintf( "%s: %q says nothing about what it is built from", module, a.Name)) } if strings.HasPrefix(a.From, "/") || strings.Contains(a.From, "..") { // A build reads its own repository and nothing else. A path leaving it would make // what gets built depend on whatever happens to be on the machine building it. problems = append(problems, fmt.Sprintf( "%s: %q is built from %q, which is outside its own repository", module, a.Name, a.From)) } } return problems } // oneOrOther keeps the message readable for one artifact and for several, because a message that // says "neither" about one thing reads as a bug in the message. func oneOrOther(n int) string { if n == 1 { return "it" } return "them" }