mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-11-05 10:37:23 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb54792cc5 | |||
| c2cfa1053c | |||
| b58c2f002b | |||
| fea0058981 | |||
| d3a4554455 | |||
| 04ce8e68d2 |
@@ -90,6 +90,7 @@ go_build:
|
||||
LAMBDA_TESTLINK_DIR := ./bin/lambda-testlink
|
||||
LAMBDA_TESTLINK_PKG := github.com/mulgadc/spinifex/spinifex/runtime/daemon
|
||||
LAMBDA_TESTLINK_PIN := $(LAMBDA_TESTLINK_PKG).lambdaHardKillSHA256
|
||||
LAMBDA_TESTLINK_GW := github.com/mulgadc/spinifex/spinifex/runtime/roles/awsgw
|
||||
LAMBDA_DIGEST_RE := ^[0-9a-f]{64}$$
|
||||
build-lambda-testlink: override export LAMBDA_IMAGE_DIGEST := $(value LAMBDA_IMAGE_DIGEST)
|
||||
build-lambda-testlink:
|
||||
@@ -104,8 +105,8 @@ build-lambda-testlink:
|
||||
@digest=$$(sha256sum $(LAMBDA_TESTLINK_DIR).tmp/hardkill.so | cut -d' ' -f1); \
|
||||
test $${#digest} -eq 64 || { echo "initializer digest missing"; exit 1; }; \
|
||||
echo "initializer sha256 $$digest"; \
|
||||
pin="-X $(LAMBDA_TESTLINK_PIN)=$$digest -X $(LAMBDA_TESTLINK_PKG).lambdaImageDigest=$$LAMBDA_IMAGE_DIGEST"; \
|
||||
LAMBDA_TESTLINK_INITIALIZER=$(abspath $(LAMBDA_TESTLINK_DIR).tmp/hardkill.so) go test -count=1 -tags lambdatestlink -ldflags "$$pin" -run '^TestLambda(HardKillPin|ImageDigestPin)$$' ./spinifex/runtime/daemon/ && \
|
||||
pin="-X $(LAMBDA_TESTLINK_PIN)=$$digest -X $(LAMBDA_TESTLINK_PKG).lambdaImageDigest=$$LAMBDA_IMAGE_DIGEST -X $(LAMBDA_TESTLINK_GW).lambdaImageDigest=$$LAMBDA_IMAGE_DIGEST"; \
|
||||
LAMBDA_TESTLINK_INITIALIZER=$(abspath $(LAMBDA_TESTLINK_DIR).tmp/hardkill.so) go test -count=1 -tags lambdatestlink -ldflags "$$pin" -run '^TestLambda(HardKillPin|ImageDigestPin)$$' ./spinifex/runtime/daemon/ ./spinifex/runtime/roles/awsgw/ && \
|
||||
go build $(GO_BUILD_MOD) -tags lambdatestlink -ldflags "$(subst -s -w ,,$(LDFLAGS)) $$pin" -o $(LAMBDA_TESTLINK_DIR).tmp/$(GO_PROJECT_NAME) cmd/spinifex/main.go
|
||||
@mkdir -p $(LAMBDA_TESTLINK_DIR)
|
||||
mv $(LAMBDA_TESTLINK_DIR).tmp/hardkill.so $(LAMBDA_TESTLINK_DIR).tmp/$(GO_PROJECT_NAME) $(LAMBDA_TESTLINK_DIR)/
|
||||
|
||||
@@ -22,6 +22,7 @@ Tenant code may be able to reach the listener, but it cannot open the agent's co
|
||||
For each generation the executor creates ephemeral self-signed host and guest certificates whose single URI SAN is `spinifex-lambda://<Region>/<account>/<function ID>/<environment ID>/<generation>/<role>`, with role `host` or `guest`.
|
||||
Each end validates the peer certificate against the one pinned peer certificate and separately requires that URI SAN with the opposite role; hostname verification is not relied on.
|
||||
ALPN carries the L-GUEST major version, `spinifex-lambda-guest/1`, and an unsupported version fails the handshake. Builds with the `lambdatestlink` tag offer only `spinifex-lambda-testlink/2` on both host and guest, whose first frame carries the execution-role credential, so a test-link end and a release end fail the handshake; release builds are unchanged.
|
||||
In the same builds, the gateway reaches the host daemon on the local abstract socket `spinifex-lambda-testlink-control/2`, and each request carries the runtime-image digest the gateway was built with; the daemon refuses a missing or different digest before starting the attempt, and a gateway and daemon on different socket names never connect.
|
||||
Under TLS 1.3 the guest's handshake can complete before the host refuses its certificate, so the guest treats the connection as accepted only after the host's first message.
|
||||
|
||||
**Bootstrap material:** the executor passes these fw_cfg entries under `opt/spinifex/lambda/`, and the agent reads them as root and unloads `qemu_fw_cfg` before user code starts.
|
||||
|
||||
@@ -49,6 +49,8 @@ type Request struct {
|
||||
CodeURL string `json:"code_url"`
|
||||
CodeSize int64 `json:"code_size"`
|
||||
CodeSHA256 string `json:"code_sha256"`
|
||||
// ImageDigest is the runtime image the gateway expects to run the code.
|
||||
ImageDigest string `json:"image_digest"`
|
||||
Payload []byte `json:"payload"`
|
||||
|
||||
// cred arrives beside the request on the control socket, never inside it.
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
// ControlName is the abstract Unix socket name the node's daemon serves. An
|
||||
// abstract name has no directory entry; the kernel frees it when the last
|
||||
// reference to the listening socket closes.
|
||||
const ControlName = "spinifex-lambda-testlink-control"
|
||||
const ControlName = "spinifex-lambda-testlink-control/2"
|
||||
|
||||
// Service accounts at each end of the control socket.
|
||||
const (
|
||||
|
||||
@@ -435,3 +435,25 @@ func TestControl_WriteErrorAfterDeliveryIsUnknown(t *testing.T) {
|
||||
require.Equal(t, []byte(`{"attempt_id":"a"}`), s.req.Load())
|
||||
require.Equal(t, testCred, s.cred.Load())
|
||||
}
|
||||
|
||||
// legacyControlName is the protocol name before the request carried the image
|
||||
// digest; an old daemon still serves it and ignores the field.
|
||||
const legacyControlName = "spinifex-lambda-testlink-control"
|
||||
|
||||
// A gateway and daemon on different control protocol names never connect, in
|
||||
// either direction. The scratch prefix isolates the binds; the names after it
|
||||
// are the production constant and its predecessor.
|
||||
func TestControl_MixedProtocolNamesNeverConnect(t *testing.T) {
|
||||
prefix := controlName(t) + "/"
|
||||
for name, tc := range map[string]struct{ daemon, gateway string }{
|
||||
"old daemon, new gateway": {prefix + legacyControlName, prefix + ControlName},
|
||||
"new daemon, old gateway": {prefix + ControlName, prefix + legacyControlName},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
s, _ := serveControl(t, tc.daemon, os.Getuid(), nil)
|
||||
_, err := client(tc.gateway, os.Getuid()).Exchange(t.Context(), []byte(`{"attempt_id":"a"}`), testCred, soon())
|
||||
require.ErrorIs(t, err, ErrNotSent)
|
||||
assert.Equal(t, int32(0), s.calls.Load())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,9 @@ func (d *Daemon) Handle(ctx context.Context, data []byte, cred Credential) Reply
|
||||
if d.Image == nil {
|
||||
return refused("runtime image not prepared")
|
||||
}
|
||||
if req.ImageDigest != d.Image.Digest {
|
||||
return refused("runtime image digest differs from the prepared image")
|
||||
}
|
||||
if !d.busy.CompareAndSwap(false, true) {
|
||||
return refused("another test-link attempt holds this node's slot")
|
||||
}
|
||||
@@ -193,7 +196,7 @@ func (d *Daemon) attempt(ctx context.Context, req Request, launch func(code []by
|
||||
if err := d.Grants.Start(ctx, req.Grant, req.Identity()); err != nil {
|
||||
if errors.Is(err, execution.ErrOutcomeUnknown) {
|
||||
slog.Error("Lambda test link: grant start outcome unknown; node held", "attempt", req.AttemptID, "err", err)
|
||||
return Reply{Outcome: OutcomeUnknown, Error: "grant start outcome unknown"}, true
|
||||
return Reply{Outcome: OutcomeUnknown, Error: "grant start outcome unknown"}, false
|
||||
}
|
||||
return refused(err.Error()), true
|
||||
}
|
||||
|
||||
@@ -121,13 +121,18 @@ func baseRequest(url string, code []byte) Request {
|
||||
return Request{
|
||||
AttemptID: "0123456789ab", RequestID: "r", Region: "ap-southeast-2", AccountID: "123456789012",
|
||||
FunctionID: "fn1", RevisionID: "rev1", Handler: "bootstrap.handler", TimeoutMs: 3000, CodeURL: url, CodeSize: int64(len(code)),
|
||||
CodeSHA256: hex.EncodeToString(sum[:]),
|
||||
CodeSHA256: hex.EncodeToString(sum[:]), ImageDigest: testImageDigest,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *granted) request(url string, code []byte) []byte {
|
||||
return g.requestFor(url, code, g.image.Digest)
|
||||
}
|
||||
|
||||
func (g *granted) requestFor(url string, code []byte, imageDigest string) []byte {
|
||||
req := baseRequest(url, code)
|
||||
req.Grant = g.att.Grant
|
||||
req.ImageDigest = imageDigest
|
||||
b, _ := json.Marshal(req)
|
||||
return b
|
||||
}
|
||||
@@ -148,6 +153,10 @@ func TestDaemon_RefusesBeforeAllocating(t *testing.T) {
|
||||
b, _ := json.Marshal(baseRequest(good.URL, code))
|
||||
return b
|
||||
}, execution.StateGranted},
|
||||
// A gateway built for another runtime image, or an old one that sends
|
||||
// no digest, is refused before Start.
|
||||
"image digest differs": {func(g *granted) []byte { return g.requestFor(good.URL, code, strings.Repeat("0", 64)) }, execution.StateGranted},
|
||||
"image digest missing": {func(g *granted) []byte { return g.requestFor(good.URL, code, "") }, execution.StateGranted},
|
||||
// Refused after Start with nothing launched: the daemon records the end.
|
||||
"digest mismatch": {func(g *granted) []byte { return g.request(wrong.URL, code) }, execution.StateEnded},
|
||||
"size mismatch": {func(g *granted) []byte { return g.request(short.URL, code) }, execution.StateEnded},
|
||||
@@ -164,6 +173,7 @@ func TestDaemon_RefusesBeforeAllocating(t *testing.T) {
|
||||
rep := g.daemon(&Daemon{IPs: ips, Taps: taps, HTTP: client, WorkDir: t.TempDir()}).Handle(context.Background(), tc.data(g), testCred)
|
||||
require.Equal(t, OutcomeRefused, rep.Outcome, rep.Error)
|
||||
require.Empty(t, ips.allocated)
|
||||
require.Empty(t, taps.created)
|
||||
require.NotContains(t, rep.Error, good.URL)
|
||||
require.Equal(t, tc.state, g.state(t))
|
||||
})
|
||||
@@ -198,6 +208,7 @@ func TestDaemon_OwnerRefusalComesBeforeAnyAllocation(t *testing.T) {
|
||||
ips := &fakeIPs{}
|
||||
rep := g.daemon(&Daemon{IPs: ips, Taps: &fakeTaps{}, HTTP: srv.Client(), WorkDir: t.TempDir()}).Handle(context.Background(), data, testCred)
|
||||
require.Equal(t, OutcomeRefused, rep.Outcome)
|
||||
require.NotContains(t, rep.Error, "image digest", "the owner, not the digest check, refuses")
|
||||
require.False(t, fetched)
|
||||
require.Empty(t, ips.allocated)
|
||||
require.Equal(t, execution.StateGranted, g.state(t))
|
||||
@@ -447,3 +458,65 @@ func TestCheckRequest_RefusesUnusableHandler(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// unconfirmedStart lets the owner write the start, then reports the outcome
|
||||
// as unknown, as a lost acknowledgement would.
|
||||
type unconfirmedStart struct{ *execution.Store }
|
||||
|
||||
func (u unconfirmedStart) Start(ctx context.Context, g execution.Grant, want execution.Identity) error {
|
||||
if err := u.Store.Start(ctx, g, want); err != nil {
|
||||
return err
|
||||
}
|
||||
return execution.ErrOutcomeUnknown
|
||||
}
|
||||
|
||||
func TestDaemon_UnknownStartOutcomeHoldsTheSlot(t *testing.T) {
|
||||
code := []byte("x")
|
||||
g := grant(t, code)
|
||||
ips, taps := &fakeIPs{}, &fakeTaps{}
|
||||
d := g.daemon(&Daemon{IPs: ips, Taps: taps, WorkDir: t.TempDir()})
|
||||
d.Grants = unconfirmedStart{g.store}
|
||||
|
||||
rep := d.Handle(context.Background(), g.request("https://h/x", code), testCred)
|
||||
require.Equal(t, OutcomeUnknown, rep.Outcome)
|
||||
require.True(t, d.busy.Load(), "an unconfirmed start may have taken the node")
|
||||
require.Empty(t, ips.allocated)
|
||||
require.Equal(t, execution.StateRunning, g.state(t))
|
||||
|
||||
rep = d.Handle(context.Background(), g.request("https://h/x", code), testCred)
|
||||
require.Equal(t, OutcomeRefused, rep.Outcome)
|
||||
require.Contains(t, rep.Error, "holds this node's slot")
|
||||
}
|
||||
|
||||
// A daemon whose image is replaced between two Invokes refuses the second
|
||||
// before Start, though the first passed the digest check.
|
||||
func TestDaemon_ImageReplacedBetweenInvokesRefusesBeforeStart(t *testing.T) {
|
||||
code := []byte("zip bytes")
|
||||
wrong := codeServer(t, []byte("other one"))
|
||||
g := grant(t, code)
|
||||
ips, taps := &fakeIPs{}, &fakeTaps{}
|
||||
d := g.daemon(&Daemon{IPs: ips, Taps: taps, HTTP: wrong.Client(), WorkDir: t.TempDir()})
|
||||
|
||||
rep := d.Handle(context.Background(), g.request(wrong.URL, code), testCred)
|
||||
require.Equal(t, OutcomeRefused, rep.Outcome)
|
||||
require.Equal(t, execution.StateEnded, g.state(t), "the first request passed the digest check and started")
|
||||
|
||||
second, err := g.store.Issue(context.Background(), execution.Attempt{
|
||||
Grant: execution.Grant{AttemptID: "0123456789ac", EnvironmentID: "env-0123456789ac", Generation: 1, Node: testNode},
|
||||
Identity: baseRequest("", code).Identity(),
|
||||
LaunchBy: time.Now().Add(time.Minute),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
d.Image = &Image{Digest: strings.Repeat("1", 64)}
|
||||
req := baseRequest(wrong.URL, code)
|
||||
req.AttemptID, req.Grant, req.ImageDigest = second.Grant.AttemptID, second.Grant, g.image.Digest
|
||||
b, err := json.Marshal(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
rep = d.Handle(context.Background(), b, testCred)
|
||||
require.Equal(t, OutcomeRefused, rep.Outcome)
|
||||
require.Contains(t, rep.Error, "image digest differs")
|
||||
require.Equal(t, execution.StateGranted, g.state(t), "the second grant was never started")
|
||||
require.Empty(t, ips.allocated)
|
||||
require.Empty(t, taps.created)
|
||||
}
|
||||
|
||||
@@ -24,17 +24,15 @@ func lambdaTestExecutor(nc *nats.Conn, node string, codes *codestore.Manager, se
|
||||
slog.Error("Lambda test link: control peer account unavailable; Invoke is not served", "err", err)
|
||||
return nil
|
||||
}
|
||||
if !testlink.ValidDigest(lambdaImageDigest) {
|
||||
slog.Error("Lambda test link: runtime image pin missing or malformed; every Invoke is refused")
|
||||
}
|
||||
return &testLinkExecutor{
|
||||
Control: &testlink.ControlClient{
|
||||
Name: testlink.ControlName,
|
||||
DaemonUID: daemonUID,
|
||||
RequestLimit: nc.MaxPayload(),
|
||||
ReplyLimit: nc.MaxPayload(),
|
||||
IOTimeout: controlWriteTimeout,
|
||||
},
|
||||
Control: testLinkControl(daemonUID, nc.MaxPayload()),
|
||||
RequestLimit: nc.MaxPayload(),
|
||||
Sessions: sessions,
|
||||
Node: node,
|
||||
ImageDigest: lambdaImageDigest,
|
||||
Codes: func(ctx context.Context) (codePresigner, error) {
|
||||
store, err := codes.Store(ctx)
|
||||
if err != nil {
|
||||
@@ -52,6 +50,21 @@ func lambdaTestExecutor(nc *nats.Conn, node string, codes *codestore.Manager, se
|
||||
}
|
||||
}
|
||||
|
||||
// testLinkControl is the gateway's client for this node's daemon socket.
|
||||
func testLinkControl(daemonUID int, limit int64) *testlink.ControlClient {
|
||||
return &testlink.ControlClient{
|
||||
Name: testlink.ControlName,
|
||||
DaemonUID: daemonUID,
|
||||
RequestLimit: limit,
|
||||
ReplyLimit: limit,
|
||||
IOTimeout: controlWriteTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
// lambdaImageDigest pins the runtime image this gateway expects the daemon
|
||||
// to run; make build-lambda-testlink sets it to the daemon's pin.
|
||||
var lambdaImageDigest string
|
||||
|
||||
// controlWriteTimeout bounds writing one request to the local daemon.
|
||||
const controlWriteTimeout = 30 * time.Second
|
||||
|
||||
|
||||
@@ -60,6 +60,8 @@ type testLinkExecutor struct {
|
||||
RequestLimit int64
|
||||
Sessions lambdaEnvironmentRoleIssuer
|
||||
Node string
|
||||
// ImageDigest is the runtime image every request requires of the daemon.
|
||||
ImageDigest string
|
||||
Codes func(context.Context) (codePresigner, error)
|
||||
Grants func(context.Context) (grantIssuer, error)
|
||||
now func() time.Time
|
||||
@@ -73,6 +75,10 @@ var _ awsapi.Executor = (*testLinkExecutor)(nil)
|
||||
// session is revoked on every path after it is issued.
|
||||
func (e *testLinkExecutor) InvokeSync(ctx context.Context, in awsapi.SyncInvocation) (awsapi.SyncResult, error) {
|
||||
rec := in.Function
|
||||
if !testlink.ValidDigest(e.ImageDigest) {
|
||||
return awsapi.SyncResult{}, &awsapi.RefusalError{Err: awserrors.Errorf(awserrors.ErrorServiceException,
|
||||
"the test execution path has no valid runtime image pin")}
|
||||
}
|
||||
// The test link cannot deliver environment variables, so it refuses a
|
||||
// function that has any before presigning, granting or sending.
|
||||
if rec.EnvCipher != "" {
|
||||
@@ -174,6 +180,7 @@ func (e *testLinkExecutor) InvokeSync(ctx context.Context, in awsapi.SyncInvocat
|
||||
CodeURL: url,
|
||||
CodeSize: rec.Code.Size,
|
||||
CodeSHA256: rec.Code.SHA256,
|
||||
ImageDigest: e.ImageDigest,
|
||||
Payload: in.Payload,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -114,8 +114,11 @@ func executor(n *fakeControl, presignErr error, store *execution.Store) *testLin
|
||||
return executorWith(n, &fakeSessions{}, presignErr, store)
|
||||
}
|
||||
|
||||
// testImageDigest stands in for the build's pinned runtime image digest.
|
||||
const testImageDigest = "ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12ab12"
|
||||
|
||||
func executorWith(n *fakeControl, sessions *fakeSessions, presignErr error, store *execution.Store) *testLinkExecutor {
|
||||
return &testLinkExecutor{Control: n, RequestLimit: n.max, Sessions: sessions, Node: "node1",
|
||||
return &testLinkExecutor{Control: n, RequestLimit: n.max, Sessions: sessions, Node: "node1", ImageDigest: testImageDigest,
|
||||
Codes: func(context.Context) (codePresigner, error) { return fakePresigner{err: presignErr}, nil },
|
||||
Grants: func(context.Context) (grantIssuer, error) { return store, nil },
|
||||
}
|
||||
@@ -161,6 +164,25 @@ func TestLambdaTestLinkExecutor_RefusesWithoutSending(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A gateway built without a valid image pin refuses before presigning,
|
||||
// granting, issuing or connecting.
|
||||
func TestLambdaTestLinkExecutor_InvalidImagePinRefusesBeforeConnecting(t *testing.T) {
|
||||
for name, pin := range map[string]string{"missing": "", "malformed": "not-a-digest"} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
n, store := &fakeControl{max: 1 << 20, reply: &testlink.Reply{Outcome: testlink.OutcomeDone}}, grantStore(t)
|
||||
sessions := &fakeSessions{}
|
||||
e := executorWith(n, sessions, nil, store)
|
||||
e.ImageDigest = pin
|
||||
_, err := e.InvokeSync(context.Background(), invocation(nil))
|
||||
var refusal *awsapi.RefusalError
|
||||
require.ErrorAs(t, err, &refusal)
|
||||
require.Empty(t, n.sent)
|
||||
require.Empty(t, sessions.issued)
|
||||
require.Empty(t, grantState(t, store), "no grant taken")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLambdaTestLinkExecutor_BusyNodeSendsNothing(t *testing.T) {
|
||||
store := grantStore(t)
|
||||
held := &fakeControl{max: 1 << 20, err: errors.New("no reply")}
|
||||
@@ -277,6 +299,7 @@ func TestLambdaTestLinkExecutor_IssuesForTheAttemptAndSendsOnTheSocket(t *testin
|
||||
var sent testlink.Request
|
||||
require.NoError(t, json.Unmarshal(n.sent[0], &sent))
|
||||
require.Equal(t, testScope.FunctionARN+":$LATEST", sent.FunctionARN, "the runtime still sees the invoked ARN")
|
||||
require.Equal(t, testImageDigest, sent.ImageDigest, "the daemon checks the gateway's pinned image")
|
||||
require.NotContains(t, string(n.sent[0]), testSecret, "the credential travels beside the request, not in it")
|
||||
require.Equal(t, testSecret, n.creds[0].SecretAccessKey)
|
||||
|
||||
@@ -399,3 +422,75 @@ func TestLambdaTestLinkExecutor_SilentDaemonIsUnknownAndRevoked(t *testing.T) {
|
||||
require.Less(t, time.Since(start), 10*time.Second)
|
||||
require.Len(t, sessions.revoked, 1)
|
||||
}
|
||||
|
||||
// withdrawFault fails Withdraw before or after the owner writes it.
|
||||
type withdrawFault struct {
|
||||
*execution.Store
|
||||
afterWrite bool
|
||||
}
|
||||
|
||||
func (w withdrawFault) Withdraw(ctx context.Context, g execution.Grant) error {
|
||||
if !w.afterWrite {
|
||||
return errors.New("owner unavailable")
|
||||
}
|
||||
if err := w.Store.Withdraw(ctx, g); err != nil {
|
||||
return err
|
||||
}
|
||||
return execution.ErrOutcomeUnknown
|
||||
}
|
||||
|
||||
// A daemon refusal with a failed Withdraw leaves the grant startable: a
|
||||
// replayed Start succeeds. A lost Withdraw acknowledgement leaves it fenced.
|
||||
func TestLambdaTestLinkExecutor_RefusalWithdrawFaults(t *testing.T) {
|
||||
for name, tc := range map[string]struct {
|
||||
afterWrite bool
|
||||
state string
|
||||
replay bool
|
||||
}{
|
||||
"fails before write": {state: execution.StateGranted, replay: true},
|
||||
"ack lost": {afterWrite: true, state: execution.StateWithdrawn},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
store := grantStore(t)
|
||||
n := &fakeControl{max: 1 << 20, reply: &testlink.Reply{Outcome: testlink.OutcomeRefused, Error: "image differs"}}
|
||||
e := executor(n, nil, store)
|
||||
e.Grants = func(context.Context) (grantIssuer, error) { return withdrawFault{store, tc.afterWrite}, nil }
|
||||
_, err := e.InvokeSync(context.Background(), invocation(nil))
|
||||
require.Error(t, err)
|
||||
require.Len(t, n.sent, 1)
|
||||
require.Equal(t, tc.state, grantState(t, store))
|
||||
|
||||
var sent testlink.Request
|
||||
require.NoError(t, json.Unmarshal(n.sent[0], &sent))
|
||||
err = store.Start(context.Background(), sent.Grant, sent.Identity())
|
||||
if tc.replay {
|
||||
require.NoError(t, err, "an unwithdrawn grant can still start")
|
||||
} else {
|
||||
require.Error(t, err, "a withdrawn grant cannot start")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The digest field counts toward the request bound: the exact encoded size
|
||||
// is sent and one byte less is refused unsent.
|
||||
func TestLambdaTestLinkExecutor_RequestBoundIncludesImageDigest(t *testing.T) {
|
||||
probe := &fakeControl{max: 1 << 20, reply: &testlink.Reply{Outcome: testlink.OutcomeDone}}
|
||||
_, err := executor(probe, nil, grantStore(t)).InvokeSync(context.Background(), invocation(make([]byte, 1000)))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, probe.sent, 1)
|
||||
require.Contains(t, string(probe.sent[0]), `"image_digest":"`+testImageDigest+`"`)
|
||||
size := int64(len(probe.sent[0]))
|
||||
|
||||
at := &fakeControl{max: size, reply: &testlink.Reply{Outcome: testlink.OutcomeDone}}
|
||||
_, err = executor(at, nil, grantStore(t)).InvokeSync(context.Background(), invocation(make([]byte, 1000)))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, at.sent, 1)
|
||||
require.Equal(t, size, int64(len(at.sent[0])), "the generated IDs are fixed-length, so the at-limit request is the probed size")
|
||||
|
||||
under := &fakeControl{max: size - 1}
|
||||
_, err = executor(under, nil, grantStore(t)).InvokeSync(context.Background(), invocation(make([]byte, 1000)))
|
||||
var refusal *awsapi.RefusalError
|
||||
require.ErrorAs(t, err, &refusal)
|
||||
require.Empty(t, under.sent)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
//go:build lambdatestlink
|
||||
|
||||
//test:in-package — reads the unexported ldflags image pin
|
||||
package awsgw
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/mulgadc/spinifex/spinifex/runtime/compute/lambda/testlink"
|
||||
)
|
||||
|
||||
// TestLambdaImageDigestPin runs from make build-lambda-testlink with the
|
||||
// daemon's ldflags: the gateway carries the same image digest as the daemon.
|
||||
func TestLambdaImageDigestPin(t *testing.T) {
|
||||
want := os.Getenv("LAMBDA_IMAGE_DIGEST")
|
||||
if want == "" {
|
||||
t.Skip("run by make build-lambda-testlink with a pinned image digest")
|
||||
}
|
||||
if lambdaImageDigest != want || !testlink.ValidDigest(lambdaImageDigest) {
|
||||
t.Fatalf("pinned image digest %q, want %q", lambdaImageDigest, want)
|
||||
}
|
||||
}
|
||||
|
||||
// The gateway dials the production control name, so a mixed-version daemon on
|
||||
// the previous name is never reached.
|
||||
func TestLambdaTestLinkControlUsesTheProductionName(t *testing.T) {
|
||||
c := testLinkControl(4242, 1<<20)
|
||||
if c.Name != testlink.ControlName || c.DaemonUID != 4242 {
|
||||
t.Fatalf("control client %q uid %d, want %q uid 4242", c.Name, c.DaemonUID, testlink.ControlName)
|
||||
}
|
||||
}
|
||||
@@ -212,7 +212,7 @@ func composedExecutor(t *testing.T, env *seamEnv, codeURL string, client *http.C
|
||||
grants := &countedGrants{grantIssuer: env.grants}
|
||||
presigns := &atomic.Int32{}
|
||||
daemon := startSeamDaemon(t, &testlink.Daemon{Node: "node1", Grants: env.grants, HTTP: client}, guest)
|
||||
exec := &testLinkExecutor{Control: daemon.client, RequestLimit: 1 << 20, Sessions: env.sts, Node: "node1",
|
||||
exec := &testLinkExecutor{Control: daemon.client, RequestLimit: 1 << 20, Sessions: env.sts, Node: "node1", ImageDigest: testImageDigest,
|
||||
Codes: func(context.Context) (codePresigner, error) { return urlPresigner{codeURL, presigns}, nil },
|
||||
Grants: func(context.Context) (grantIssuer, error) { return grants, nil },
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user