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 ArtifactPackage: // A package is not a resource on any machine; it is consumed by other builds. A // resource that names one is a manifest error, named here rather than shipped. return Manifest{}, fmt.Errorf( "%s: %v uses %q, which is a package — a build input, not a resource a machine runs", m.Module, r["id"], named) case ArtifactImage, ArtifactUpstream: filled["image"] = artifact.Reference 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 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, ArtifactBundle) } 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 switch a.Kind { case ArtifactImage, ArtifactArchive, ArtifactUpstream, ArtifactBundle, ArtifactPackage: default: problems = append(problems, fmt.Sprintf( "%s: %q is a %q, and an artifact is %q, %q, %q or %q", module, a.Name, a.Kind, ArtifactImage, ArtifactArchive, ArtifactUpstream, ArtifactBundle+", "+ArtifactPackage)) } // **A bundle is built from the module itself, so it says a language instead.** Everything // else names what it is built from: a Dockerfile, a directory, somebody else's reference. // A bundle's source is the module's own directory by definition, and what it needs to say // is which compiler — because the mesh chooses that, and cannot choose for a module that // has not said. if a.Kind == ArtifactBundle || a.Kind == ArtifactPackage { if a.From != "" { problems = append(problems, fmt.Sprintf( "%s: %q is a bundle and names what it is built from (%q). A bundle is built "+ "from the module's own directory; what it says is the language", module, a.Name, a.From)) } if strings.TrimSpace(a.Language) == "" { problems = append(problems, fmt.Sprintf( "%s: %q is a bundle and says no language, so nothing can choose a compiler "+ "for it", module, a.Name)) } } else { if a.From == "" { problems = append(problems, fmt.Sprintf( "%s: %q says nothing about what it is built from", module, a.Name)) } if a.Language != "" { problems = append(problems, fmt.Sprintf( "%s: %q is a %q and names a language. Only a bundle is compiled by the mesh; "+ "everything else brings its own recipe", module, a.Name, a.Kind)) } } // An upstream image is named, not read from the repository, so the path rule does not // apply to it — and applying it anyway would refuse every reference with a registry host // in it. if a.Kind != ArtifactUpstream && (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)) } if a.Kind == ArtifactUpstream && !strings.Contains(a.From, ":") { // Without a tag or digest, what gets mirrored is whatever `latest` means today, and // a module pinned to that is not pinned. problems = append(problems, fmt.Sprintf( "%s: %q mirrors %q, which names no tag or digest", 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" }