The mesh names what may go from its own build records — a digest it did not record making is never named, which is what keeps the sweep away from the images genesis pushed. An artifact stays because a definition the mesh holds names it, or because it belongs to one of the five most recent successful builds of its module. internal/artifacts asks the store to let go of one; internal/inventory decides and remembers (migration 0055); the sweep runs after a build the mesh recorded, which is when both the bytes and the keep set moved. Never fatal to a build. And the manifest side of while-stopped, refused from the definition alone: no schedule, run-once, a container the module does not declare, itself.
100 lines
3.9 KiB
Go
100 lines
3.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"
|
|
"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 = fmt.Errorf("the store does not hold it")
|
|
|
|
// 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 {
|
|
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.
|
|
return fmt.Errorf("%s is not a reference into the mesh's artifact store", 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("%q names nothing the store holds by digest", path)
|
|
}
|