Files
mesh-delivery/modules/mesh-delivery-sdk/steprun/storedbuilds.go
T
jschoubben 3e61b70517 SD5, BG11, BLD10: the build modules' shared code moves into the delivery SDK
All of mesh-build-go's buildstep Go module leaves mesh-catalog for
modules/mesh-delivery-sdk: the step run, fetch and release and the
regenerate-and-compare step as the package steprun, the CPU share as
steprun/buildcpu, the conformance kit as steprun/conformance, the seat
generator as seatgen with cmd/seatgen, and cmd/mesh-pack. The three
build modules and the builder's and checker's tests import the SDK and
keep no copy; the SDK requires nothing of mesh-catalog. Code the build
modules share is the delivery domain's SDK, not one module's package.
It carries the build-module round's two new files of that package
(dropdetected.go 247c422f, storedbuilds.go 904b8aed).

Builds: design 47 G §464, §680, §327, §420, §495

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 05:18:34 +01:00

117 lines
4.1 KiB
Go

package steprun
// **A group run's package members, read from the artifact store** (design 47 G §465, §373): the ask's fetch names each
// member's build by its digest; the build module reads it from the artifact store, through the provision it requires
// (`artifact-store`, bound to a file holding `at` and `serves.port`), at
// `artifact-store://<module>/<artifact>/blobs/<digest>`, that is GET `/v2/<module>/<artifact>/blobs/<digest>`, and checks
// the bytes against the digest. It only reads: nothing writes into the store but its holder (ADR 0614 §2).
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"regexp"
"strings"
sdkstep "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
)
// MaxStoredBuild is the most bytes of one member's build the module reads.
const MaxStoredBuild = 512 << 20
var blobDigest = regexp.MustCompile(`^sha256:[0-9a-f]{64}$`)
// BuildsArg reads a fetch's `builds` argument (the part build-step's Fetch.builds): a list of {package, module,
// artifact, digest}, as JSON gives it or as the ask's own form. nil for none.
func BuildsArg(v any) ([]sdkstep.FetchBuild, error) {
switch v := v.(type) {
case nil:
return nil, nil
case []sdkstep.FetchBuild:
return append([]sdkstep.FetchBuild(nil), v...), check(v)
case []any:
raw, err := json.Marshal(v)
if err != nil {
return nil, err
}
var out []sdkstep.FetchBuild
if err := json.Unmarshal(raw, &out); err != nil {
return nil, fmt.Errorf("builds is a list of {package, module, artifact, digest}: %w", err)
}
return out, check(out)
}
return nil, fmt.Errorf("builds is a list of {package, module, artifact, digest}")
}
func check(bs []sdkstep.FetchBuild) error {
for _, b := range bs {
if b.Package == "" || b.Module == "" || b.Artifact == "" || !blobDigest.MatchString(b.Digest) {
return fmt.Errorf("builds: %+v names no package, module, artifact or sha256: digest", b)
}
}
return nil
}
// StoreURL is the artifact store's base URL from its binding file (`at`, and `serves` scheme and port), as the Go
// seat's binding reads; an error naming the file when it is not bound.
func StoreURL(bindFile string) (string, error) {
raw, err := os.ReadFile(bindFile)
if err != nil {
return "", fmt.Errorf("the artifact store is not bound (%v)", err)
}
var b struct {
At string `json:"at"`
Serves struct {
Scheme string `json:"scheme"`
Port int `json:"port"`
} `json:"serves"`
}
if json.Unmarshal(raw, &b) != nil || b.At == "" {
return "", fmt.Errorf("the artifact store's binding %s names no address", bindFile)
}
scheme := b.Serves.Scheme
if scheme == "" {
scheme = "http"
}
if b.Serves.Port == 0 {
return fmt.Sprintf("%s://%s", scheme, b.At), nil
}
return fmt.Sprintf("%s://%s:%d", scheme, b.At, b.Serves.Port), nil
}
// ReadStoredBuild reads one member's build from the store at base and checks its bytes against its digest; a failure
// names the member.
func ReadStoredBuild(ctx context.Context, client *http.Client, base string, b sdkstep.FetchBuild) ([]byte, error) {
member := fmt.Sprintf("the build of %s (%s %s at %s)", b.Package, b.Module, b.Artifact, b.Digest)
url := strings.TrimRight(base, "/") + "/v2/" + b.Module + "/" + b.Artifact + "/blobs/" + b.Digest
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("%s: %w", member, err)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("%s: the artifact store did not answer: %w", member, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s: the artifact store answered %s", member, resp.Status)
}
data, err := io.ReadAll(io.LimitReader(resp.Body, MaxStoredBuild+1))
if err != nil {
return nil, fmt.Errorf("%s: reading it: %w", member, err)
}
if len(data) > MaxStoredBuild {
return nil, fmt.Errorf("%s: larger than %d bytes", member, MaxStoredBuild)
}
sum := sha256.Sum256(data)
if got := "sha256:" + hex.EncodeToString(sum[:]); got != b.Digest {
return nil, fmt.Errorf("%s: the bytes read are %s, not the digest asked", member, got)
}
return data, nil
}