feat(build): make the user-modpack build lane read its context (closes the last functional gap)
A submitted modpack was durable but unreadable: the uploads PVC cannot cross
namespaces (felis-api mounts it; Kaniko runs in felis-build) and the s3 lane
handed the sandboxed build Pod no credentials, so NO user build could ever
consume its context. The transport is now the API itself:
- submit: derived context refs become the internal-face URL
/api/v1/internal/submissions/{id}/context (service-token gated), and Blobs
gains Open (local + s3) with an ErrBlobNotFound sentinel for the route's 404.
- api: serves that route on the internal face only (openapi.yaml updated; the
route-coverage test enforces it).
- build: an http(s) context renders a context-fetch initContainer (the felis
image's new fetch-context entrypoint) that streams the blob with the
namespace-local service-token Secret — never mounted into Kaniko — and
extracts it under a zip-slip guard into a size-limited emptyDir that Kaniko
reads read-only as --context=/context.
- platform/install: the api Deployment carries its own internal base URL; the
build namespace gets the token Secret through the existing replica mechanism
(bootstrap.sh + felis setup); the build egress lock opens exactly the control
namespace on the internal port.
- cmd/felis: fetch-context entrypoint (registered, documented, unit-tested for
escapes/symlinks/non-gzip).
Tests cover rendering, hardening, the s3/local Open paths, and the route's
404/503 mapping. Verified next on the real single-node cluster with Kaniko.
This commit is contained in:
26 files changed
+1080
-72
No files matched your search
@@ -1,8 +1,10 @@
|
||||
package submit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -50,6 +52,14 @@ func (f *fakeBlobs) Exists(_ context.Context, id string) (bool, error) {
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
func (f *fakeBlobs) Open(_ context.Context, id string) (io.ReadCloser, error) {
|
||||
b, ok := f.stored[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: no blob for %s", ErrBlobNotFound, id)
|
||||
}
|
||||
return io.NopCloser(bytes.NewReader(b)), nil
|
||||
}
|
||||
|
||||
// testNow is the frozen clock for hermetic assertions.
|
||||
var testNow = time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
@@ -217,6 +227,52 @@ func TestCreatePendingDoesNotBuild(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// With a ContextBaseURL the derived ref is the internal-face URL the build Pod's
|
||||
// fetch initContainer dials — not a filesystem path it could never read across
|
||||
// namespaces. OpenContext then serves whatever the Blobs transport stored.
|
||||
func TestContextRefIsFetchURLAndOpenContextServesIt(t *testing.T) {
|
||||
m, _, _ := newManager()
|
||||
m.ContextBaseURL = "http://felis-api-internal.felis.svc.cluster.local:8081/"
|
||||
m.Blobs = newFakeBlobs()
|
||||
ctx := context.Background()
|
||||
|
||||
sub, err := m.Create(ctx, CreateRequest{DisplayName: "Pack", SubmittedBy: "user-1"})
|
||||
if err != nil {
|
||||
t.Fatalf("Create: %v", err)
|
||||
}
|
||||
want := "http://felis-api-internal.felis.svc.cluster.local:8081/api/v1/internal/submissions/sub-1/context"
|
||||
if sub.ContextRef != want {
|
||||
t.Fatalf("context_ref = %q, want the internal fetch URL %q", sub.ContextRef, want)
|
||||
}
|
||||
|
||||
// Before any upload the read path reports not-found (the route's 404).
|
||||
if _, err := m.OpenContext(ctx, sub.ID); !errors.Is(err, ErrBlobNotFound) {
|
||||
t.Fatalf("OpenContext before upload = %v, want ErrBlobNotFound", err)
|
||||
}
|
||||
payload := "\x1f\x8b\x08\x00payload"
|
||||
if _, err := m.UploadContext(ctx, sub.ID, "user-1", strings.NewReader(payload)); err != nil {
|
||||
t.Fatalf("UploadContext: %v", err)
|
||||
}
|
||||
rc, err := m.OpenContext(ctx, sub.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenContext: %v", err)
|
||||
}
|
||||
defer rc.Close()
|
||||
got, _ := io.ReadAll(rc)
|
||||
if string(got) != payload {
|
||||
t.Fatalf("OpenContext served %q, want %q", got, payload)
|
||||
}
|
||||
}
|
||||
|
||||
// No upload transport ⇒ no readable blob: the route reports the same 503 the
|
||||
// upload endpoint does, rather than a misleading 404.
|
||||
func TestOpenContextWithoutTransportIsUnavailable(t *testing.T) {
|
||||
m, _, _ := newManager()
|
||||
if _, err := m.OpenContext(context.Background(), "sub-1"); !errors.Is(err, ErrUploadsUnavailable) {
|
||||
t.Fatalf("OpenContext with nil Blobs = %v, want ErrUploadsUnavailable", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateValidation(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
|
||||
Reference in new issue
Block a user