154 lines
7.1 KiB
Go
154 lines
7.1 KiB
Go
package main
|
|
|
|
// **The sweep asks the registry seats** (CAT-AS1; novox/hq ADR 0617 §2, design 51): the artifact store's write side is
|
|
// its holder's, so the sweep no longer deletes or holds over the store's door. What it keeps from retention it asks the
|
|
// seat's `hold` for (a kept bundle or archive on oci-artifact-registry, a kept copy of an upstream base on
|
|
// container-image-registry), and what is eligible it asks the seat's `remove` for, with the sweep's reason; both verbs
|
|
// are in the manifest's `invokes`, and only this module is on their caller lists. A reference is read in both forms
|
|
// (ADR 0605 §9): the older `artifact-store://<path>` and the seat's `<seat>/<module>/<artifact>@<digest>`; the keep set
|
|
// keys each by the older form, so the two meet as one artifact. A call a seat refuses stops the sweep and writes the
|
|
// sweep's finding, sweep-refused; the next sweep with no refusal clears it.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient"
|
|
meshconditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
|
|
artifactref "git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient/reference"
|
|
ociseat "git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
|
|
reg "git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry"
|
|
)
|
|
|
|
// SweepRefused is the sweep's finding: a seat refused a hold or a remove the sweep asked.
|
|
const SweepRefused = "sweep-refused"
|
|
|
|
// sweepFindingKey is the one key of the sweep's finding in the module's findings.
|
|
var sweepFindingKey = meshconditionsclient.Key(meshconditionsclient.ScopeModule, Module, SweepRefused)
|
|
|
|
// canonical is a kept reference in the older form, `artifact-store://<path>`, whichever form it came in; another is as
|
|
// it was.
|
|
func canonical(reference string) string {
|
|
path, kept := registryForms.InArtifactStore(reference)
|
|
if !kept {
|
|
return reference
|
|
}
|
|
return catalogue.ArtifactStoreScheme + path
|
|
}
|
|
|
|
// seatOf is the seat, name and digest a kept reference is held under: a bundle or an archive on oci-artifact-registry,
|
|
// an image or a copy of an upstream base on container-image-registry.
|
|
func seatOf(reference string) (seat, name, digest string, err error) {
|
|
path, kept := registryForms.InArtifactStore(reference)
|
|
if !kept {
|
|
return "", "", "", fmt.Errorf("%s is no reference into the mesh's registries", reference)
|
|
}
|
|
if name, digest, ok := strings.Cut(path, "/blobs/"); ok {
|
|
return ociseat.Seat, name, digest, nil
|
|
}
|
|
if name, digest, ok := strings.Cut(path, "@"); ok {
|
|
return reg.Seat, name, digest, nil
|
|
}
|
|
return "", "", "", fmt.Errorf("%s names no digest", reference)
|
|
}
|
|
|
|
// isCopy is whether a kept reference is a copy of an upstream base.
|
|
func isCopy(reference string) bool {
|
|
path, _ := registryForms.InArtifactStore(reference)
|
|
return strings.HasPrefix(path, "upstream/")
|
|
}
|
|
|
|
// askSeat asks verb of the seat reference is held on, with body built from its name and digest.
|
|
func (h *Holder) askSeat(reference, verb string, body func(name, digest string) any, answer any) error {
|
|
seat, name, digest, err := seatOf(reference)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if h.Seats == nil {
|
|
return fmt.Errorf("this process reaches no registry seat")
|
|
}
|
|
raw, err := h.Seats("seat:"+seat+"."+verb, body(name, digest))
|
|
if err != nil {
|
|
return fmt.Errorf("%s's %s of %s@%s: %w", seat, verb, name, digest, err)
|
|
}
|
|
if answer != nil && len(raw) > 0 {
|
|
if err := json.Unmarshal(raw, answer); err != nil {
|
|
return fmt.Errorf("%s's answer to %s: %w", seat, verb, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// holdOnSeat asks the seat to keep reference from retention, for why.
|
|
func (h *Holder) holdOnSeat(reference, why string) (bool, error) {
|
|
var a reg.HoldAnswer
|
|
err := h.askSeat(reference, reg.VerbHold, func(name, digest string) any {
|
|
return reg.HoldInput{Name: name, Digest: digest, Reason: why}
|
|
}, &a)
|
|
if err == nil && !a.Held {
|
|
err = fmt.Errorf("%s: the seat answered it is not held", reference)
|
|
}
|
|
return a.Held, err
|
|
}
|
|
|
|
// Retention is the reason of a removal by digest, the sweep's alone (design 51 *Who may call which verb*): the holder
|
|
// removes an untagged thing only for it. The seat's definition names it.
|
|
const Retention = reg.RemoveInputReasonRetention
|
|
|
|
// removeOnSeat lets reference go: a hold an earlier sweep asked while it was kept is released first (the holder
|
|
// refuses to remove a held thing; one not held answers released false), then it is removed for retention. why goes with
|
|
// the release; the hand act records it.
|
|
func (h *Holder) removeOnSeat(reference, why string) error {
|
|
if err := h.askSeat(reference, reg.VerbRelease, func(name, digest string) any {
|
|
return reg.ReleaseInput{Name: name, Digest: digest, Reason: why}
|
|
}, ®.ReleaseAnswer{}); err != nil {
|
|
return err
|
|
}
|
|
return h.askSeat(reference, reg.VerbRemove, func(name, digest string) any {
|
|
return reg.RemoveInput{Name: name, Digest: digest, Reason: Retention}
|
|
}, ®.RemoveAnswer{})
|
|
}
|
|
|
|
// sweepFinding writes the sweep's finding: what the seat refused, and why.
|
|
func (h *Holder) sweepFinding(what string, err error) {
|
|
at := h.now().UTC()
|
|
f := meshconditions.FindingsByKeyValue{V: meshconditionsclient.FindingVersion, Key: sweepFindingKey, Kind: SweepRefused,
|
|
Subject: meshconditions.Subject{Scope: meshconditionsclient.ScopeModule, ID: Module},
|
|
Severity: meshconditionsclient.Warning, Resolver: meshconditionsclient.ResolverOperator,
|
|
Summary: fmt.Sprintf("the artifact sweep stopped: a registry seat refused %s (the evidence names why)", what),
|
|
Headline: "The artifact sweep stopped",
|
|
Explanation: "The sweep keeps what the mesh still needs and removes the rest through the registry seats. A seat " +
|
|
"refused one of its asks, so it stopped before anything more was removed; what it kept stays kept.",
|
|
Needs: "read the seat holder's journal for the refusal; the next sweep asks again.",
|
|
Resolved: "Resolved: the artifact sweep ran through without a refusal",
|
|
Evidence: []meshconditions.Evidence{{At: at.UTC(), Said: fmt.Sprintf("%s: %v", what, err)}},
|
|
Source: "CAT-AS1", Observer: Module, At: at}
|
|
if werr := h.Bus.Put(FindingsState, sweepFindingKey, f); werr != nil {
|
|
h.logf("the sweep's finding (%s: %v) could not be written: %v", what, err, werr)
|
|
}
|
|
}
|
|
|
|
// sweepClean clears the sweep's finding after a sweep no seat refused.
|
|
func (h *Holder) sweepClean() {
|
|
if _, found, err := h.Bus.Get(FindingsState, sweepFindingKey); err == nil && found {
|
|
if err := h.Bus.Delete(FindingsState, sweepFindingKey); err != nil {
|
|
h.logf("the sweep's finding could not be cleared: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// keptWhy is why a kept reference is held, as the seat's hold takes it.
|
|
func keptWhy(r *Ref) string {
|
|
if r == nil || len(r.Why) == 0 {
|
|
return "kept by the artifact sweep"
|
|
}
|
|
return "kept: " + strings.Join(r.Why, "; ")
|
|
}
|
|
|
|
// registryForms reads and routes a reference of the registry form, with the two registry seats' names from their contracts
|
|
// (the registry clients' reference forms, novox/hq ADR 0605 §9, ADR 0634 §4).
|
|
var registryForms = artifactref.Seats{Image: reg.Seat, Object: ociseat.Seat}
|