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
117 lines
4.1 KiB
Go
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
|
|
}
|