Issue 225. The mesh seals one credential per consumer beside the provider's contributions file, and wrote it root-owned. That was right while a module's own code ran in a container as root; ADR 0198 moved that code under the node's runtime, as the node's account, and the secret stayed root's. On the control machine two consumers went unprovisioned for three hours and the only sign was a line reading 'secret not readable yet', 4330 times. The same sentence is already written for a module's own secrets a few hundred lines above — 'a root-owned 0600 file is one that process cannot read'. This is that rule reaching the other kind of secret the mesh writes for a module. Issue 226. The sweep met a reference recorded with the store's old address, read 'I will not address this' as 'the store refuses everything', and collected none of the 1681 it had found. Two changes: references from build records are read through Recorded, where the provenance is known — not in LetGo, which cannot tell one registry host from another and must stay strict — and a reference the sweep will not address is now ErrNotOurs, skipped, never a reason to stop. Only the store refusing ends a sweep. make check: the two failures both fail on main as well — the resolver test (hq 202/203) and the service-manager test, which reads this machine's own shell environment.
114 lines
4.9 KiB
Go
114 lines
4.9 KiB
Go
// Package artifacts speaks to the mesh's artifact store over its own door.
|
|
//
|
|
// Only what the mesh needs that nothing else does: letting go of something it put there
|
|
// (novox/hq ADR 0189, issue 108). Pushing is the builder's, through the container runtime; reading
|
|
// is every machine's, through its runtime. This is the one operation that belongs to the thing
|
|
// holding the records, because it is the only one that is a decision rather than a transfer.
|
|
package artifacts
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// Store is the artifact store at an address, as this machine reaches it.
|
|
type Store struct {
|
|
// Address is `host:port` — the store as the caller reaches it now, composed and never
|
|
// recorded (novox/hq 04-ISSUES/102).
|
|
Address string
|
|
// HTTP is the client used; nil is a client with a modest timeout.
|
|
HTTP *http.Client
|
|
}
|
|
|
|
// Gone is the answer when the store does not hold it: the outcome wanted, already true.
|
|
var Gone = errors.New("the store does not hold it")
|
|
|
|
// ErrNotOurs is a reference this sweep will not address: not the mesh's own, or naming nothing
|
|
// the store holds by digest.
|
|
//
|
|
// **A fact about the record, not about the store** (novox/hq issue 226). The two deserve opposite
|
|
// responses — skip one and go on, abandon the sweep for the other — and collapsing them into "an
|
|
// error" is how a cautious loop became one that did nothing while reporting the right number.
|
|
var ErrNotOurs = errors.New("not a reference into the mesh's artifact store")
|
|
|
|
// LetGo asks the store to drop one artifact the mesh recorded making.
|
|
//
|
|
// Takes a reference as the mesh records it — `artifact-store://<module>/<artifact>@sha256:…` for
|
|
// an image, `…/blobs/sha256:…` for an archive — because that is the identity every record uses,
|
|
// and composes the address here at the moment of use.
|
|
//
|
|
// Returns Gone when the store answers that it does not have it. That is not a failure: the sweep
|
|
// wants the artifact absent, and it is. It is distinguished from success only so a caller can say
|
|
// which of the two happened.
|
|
func (s Store) LetGo(ctx context.Context, reference string) error {
|
|
// **Strict, and deliberately** (novox/hq issue 226). Only a reference the mesh keeps in its
|
|
// own vocabulary is addressed here. `Recorded` would read `docker.io/library/registry@sha256:…`
|
|
// as the mesh's too — it cannot tell one registry host from another — so normalising belongs
|
|
// where the provenance is known, which is the sweep reading its own build records, not here
|
|
// where the only job is to refuse anything that is not plainly ours.
|
|
path, kept := catalogue.InArtifactStore(reference)
|
|
if !kept {
|
|
// Nothing the mesh put in its own store. Refused rather than attempted: composing a
|
|
// delete for a reference of unknown shape is how a sweep reaches something that is not
|
|
// the mesh's. Distinguished from a store that refuses, so a sweep skips this and goes on.
|
|
return fmt.Errorf("%w: %s", ErrNotOurs, reference)
|
|
}
|
|
if s.Address == "" {
|
|
return fmt.Errorf("this mesh has no artifact store on its network to ask about %s", reference)
|
|
}
|
|
repository, kind, digest, err := split(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
url := "http://" + s.Address + "/v2/" + repository + "/" + kind + "/" + digest
|
|
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
client := s.HTTP
|
|
if client == nil {
|
|
client = &http.Client{Timeout: 30 * time.Second}
|
|
}
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close()
|
|
switch response.StatusCode {
|
|
case http.StatusAccepted, http.StatusOK, http.StatusNoContent:
|
|
return nil
|
|
case http.StatusNotFound:
|
|
return Gone
|
|
case http.StatusMethodNotAllowed:
|
|
// The registry was started without deletion enabled. Said plainly, because the remedy is
|
|
// a setting on the store's module and not anything about this artifact.
|
|
return fmt.Errorf(
|
|
"the artifact store refuses deletion: its server was started without it enabled "+
|
|
"(REGISTRY_STORAGE_DELETE_ENABLED), so nothing can be collected until the store "+
|
|
"module is applied again (novox/hq ADR 0189). Asking about %s", reference)
|
|
default:
|
|
return fmt.Errorf("the artifact store answered %s for %s", response.Status, reference)
|
|
}
|
|
}
|
|
|
|
// split reads a recorded path into the repository, which endpoint names the thing, and the digest.
|
|
//
|
|
// Two shapes, which are the two the mesh records: `<repository>@sha256:<hex>` is a manifest, and
|
|
// `<repository>/blobs/sha256:<hex>` is a blob.
|
|
func split(path string) (repository, kind, digest string, err error) {
|
|
if before, after, ok := strings.Cut(path, "@sha256:"); ok {
|
|
return before, "manifests", "sha256:" + after, nil
|
|
}
|
|
if before, after, ok := strings.Cut(path, "/blobs/sha256:"); ok {
|
|
return before, "blobs", "sha256:" + after, nil
|
|
}
|
|
return "", "", "", fmt.Errorf("%w: %q names nothing the store holds by digest", ErrNotOurs, path)
|
|
}
|