Keep run logs opaque and streamline transfers
This commit is contained in:
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
@@ -10,7 +11,7 @@ import (
|
||||
"browser.local/run/protocol"
|
||||
)
|
||||
|
||||
func TestPlatformClientArtifactMethodsPostJSONAndDecodeResponses(t *testing.T) {
|
||||
func TestPlatformClientArtifactMethodsUploadRawChunksAndDecodeResponses(t *testing.T) {
|
||||
seen := map[string]bool{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
seen[r.URL.Path] = true
|
||||
@@ -23,10 +24,16 @@ func TestPlatformClientArtifactMethodsPostJSONAndDecodeResponses(t *testing.T) {
|
||||
}
|
||||
writeTestJSON(t, w, validArtifactOpenResponse())
|
||||
case "/api/v1/run/artifacts/chunks":
|
||||
var request protocol.ArtifactChunkUploadRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
if request.TransferID != "transfer-1" || request.ChunkIndex != 0 || string(request.Payload) != "payload" {
|
||||
t.Fatalf("unexpected artifact chunk request: %+v", request)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read artifact chunk body: %v", err)
|
||||
}
|
||||
verifyRunRequestSignature(t, r, body, "run-local", "session-token")
|
||||
if r.Header.Get("Content-Type") != "application/octet-stream" || r.Header.Get("X-Run-Session-Token") != "session-token" {
|
||||
t.Fatalf("unexpected artifact chunk content headers: %+v", r.Header)
|
||||
}
|
||||
if r.Header.Get("X-Artifact-Transfer-Id") != "transfer-1" || r.Header.Get("X-Artifact-Chunk-Index") != "0" || string(body) != "payload" {
|
||||
t.Fatalf("unexpected artifact chunk request headers=%+v body=%q", r.Header, string(body))
|
||||
}
|
||||
writeTestJSON(t, w, protocol.ArtifactChunkUploadResponse{Accepted: true, TransferID: "transfer-1", ArtifactID: "artifact-1", ChunkIndex: 0, ReceivedChunkIndexes: []int{0}, NextMissingChunkIndex: 1, ServerTime: fixedClientTestTime()})
|
||||
case "/api/v1/run/artifacts/status":
|
||||
@@ -95,13 +102,13 @@ func TestPlatformClientArtifactMethodReturnsErrorForPlatformFailure(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtifactChunkPayloadUsesJSONBase64Encoding(t *testing.T) {
|
||||
func TestArtifactChunkPayloadIsNotJSONEncoded(t *testing.T) {
|
||||
encoded, err := json.Marshal(validClientArtifactChunk())
|
||||
if err != nil {
|
||||
t.Fatalf("marshal artifact chunk: %v", err)
|
||||
}
|
||||
if !json.Valid(encoded) || !containsJSONPayloadField(encoded) {
|
||||
t.Fatalf("expected JSON encoded payload field, got %s", string(encoded))
|
||||
if !json.Valid(encoded) || containsJSONPayloadField(encoded) {
|
||||
t.Fatalf("artifact chunk payload must stay out of JSON, got %s", string(encoded))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -20,14 +21,16 @@ func TestPlatformClientLightweightChannelsCompleteWhileArtifactChunkIsBlocked(t
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/run/artifacts/chunks":
|
||||
var request protocol.ArtifactChunkUploadRequest
|
||||
decodeTestRequest(t, r, &request)
|
||||
if request.ChunkIndex != 0 || len(request.Payload) == 0 {
|
||||
t.Fatalf("unexpected artifact payload: %+v", request)
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read artifact payload: %v", err)
|
||||
}
|
||||
if r.Header.Get("X-Artifact-Chunk-Index") != "0" || len(body) == 0 || r.Header.Get("Content-Type") != "application/octet-stream" {
|
||||
t.Fatalf("unexpected artifact payload headers=%+v body=%q", r.Header, string(body))
|
||||
}
|
||||
close(artifactStarted)
|
||||
<-releaseArtifact
|
||||
writeTestJSON(t, w, protocol.ArtifactChunkUploadResponse{Accepted: true, TransferID: request.TransferID, ArtifactID: request.ArtifactID, ChunkIndex: request.ChunkIndex, ReceivedChunkIndexes: []int{0}, NextMissingChunkIndex: 1, ServerTime: fixedClientTestTime()})
|
||||
writeTestJSON(t, w, protocol.ArtifactChunkUploadResponse{Accepted: true, TransferID: r.Header.Get("X-Artifact-Transfer-Id"), ArtifactID: r.Header.Get("X-Artifact-Id"), ChunkIndex: 0, ReceivedChunkIndexes: []int{0}, NextMissingChunkIndex: 1, ServerTime: fixedClientTestTime()})
|
||||
close(artifactDone)
|
||||
case "/api/v1/run/control/heartbeat":
|
||||
var request protocol.RunHeartbeatRequest
|
||||
|
||||
+65
-1
@@ -31,6 +31,16 @@ type PlatformClient struct {
|
||||
serverClockOffsetActive *atomic.Bool
|
||||
}
|
||||
|
||||
const (
|
||||
runSessionTokenHeader = "X-Run-Session-Token"
|
||||
artifactTransferIDHeader = "X-Artifact-Transfer-Id"
|
||||
artifactIDHeader = "X-Artifact-Id"
|
||||
artifactChunkIndexHeader = "X-Artifact-Chunk-Index"
|
||||
artifactChunkOffsetHeader = "X-Artifact-Offset"
|
||||
artifactChunkSizeHeader = "X-Artifact-Size"
|
||||
artifactChunkHashHeader = "X-Artifact-Checksum"
|
||||
)
|
||||
|
||||
type PlatformRequestError struct {
|
||||
Status int
|
||||
Path string
|
||||
@@ -326,7 +336,7 @@ func (c PlatformClient) OpenArtifactTransfer(ctx context.Context, request protoc
|
||||
}
|
||||
|
||||
func (c PlatformClient) UploadArtifactChunk(ctx context.Context, request protocol.ArtifactChunkUploadRequest) (protocol.ArtifactChunkUploadResponse, error) {
|
||||
return postPlatformJSON[protocol.ArtifactChunkUploadRequest, protocol.ArtifactChunkUploadResponse](ctx, c, "/api/v1/run/artifacts/chunks", request)
|
||||
return postPlatformArtifactChunk(ctx, c, "/api/v1/run/artifacts/chunks", request)
|
||||
}
|
||||
|
||||
func (c PlatformClient) QueryArtifactTransferStatus(ctx context.Context, request protocol.ArtifactTransferStatusRequest) (protocol.ArtifactTransferStatusResponse, error) {
|
||||
@@ -337,6 +347,56 @@ func (c PlatformClient) CompleteArtifactTransfer(ctx context.Context, request pr
|
||||
return postPlatformJSON[protocol.ArtifactTransferCompleteRequest, protocol.ArtifactTransferCompleteResponse](ctx, c, "/api/v1/run/artifacts/complete", request)
|
||||
}
|
||||
|
||||
func postPlatformArtifactChunk(ctx context.Context, client PlatformClient, path string, request protocol.ArtifactChunkUploadRequest) (protocol.ArtifactChunkUploadResponse, error) {
|
||||
var response protocol.ArtifactChunkUploadResponse
|
||||
startedAt := time.Now()
|
||||
log.Printf("RUN platform request status=starting method=POST base=%s path=%s", diagnosticLogValue(client.baseURL), path)
|
||||
|
||||
body := request.Payload
|
||||
httpRequest, err := http.NewRequestWithContext(ctx, http.MethodPost, client.baseURL+path, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
log.Printf("RUN platform request status=build_error method=POST base=%s path=%s durationMs=%d error=%s", diagnosticLogValue(client.baseURL), path, time.Since(startedAt).Milliseconds(), err)
|
||||
return response, fmt.Errorf("build platform request: %w", err)
|
||||
}
|
||||
httpRequest.Header.Set("Content-Type", "application/octet-stream")
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set(runSessionTokenHeader, request.SessionToken)
|
||||
httpRequest.Header.Set(artifactTransferIDHeader, request.TransferID)
|
||||
httpRequest.Header.Set(artifactIDHeader, request.ArtifactID)
|
||||
httpRequest.Header.Set(artifactChunkIndexHeader, strconv.Itoa(request.ChunkIndex))
|
||||
httpRequest.Header.Set(artifactChunkOffsetHeader, strconv.FormatInt(request.Offset, 10))
|
||||
httpRequest.Header.Set(artifactChunkSizeHeader, strconv.Itoa(request.SizeBytes))
|
||||
httpRequest.Header.Set(artifactChunkHashHeader, request.Checksum)
|
||||
signatureSummary, err := signRunRequestWithEnvelope(httpRequest, body, runRequestEnvelope{RunEndpointID: request.RunEndpointID, SessionToken: request.SessionToken}, client.signatureTime())
|
||||
if err != nil {
|
||||
log.Printf("RUN platform request status=sign_error method=POST base=%s path=%s durationMs=%d error=%s", diagnosticLogValue(client.baseURL), path, time.Since(startedAt).Milliseconds(), err)
|
||||
return response, err
|
||||
}
|
||||
log.Printf("RUN platform request status=signed method=POST base=%s path=%s endpoint=%s timestamp=%s nonce=%s bodyHash=%s signature=%s", diagnosticLogValue(client.baseURL), path, diagnosticLogValue(signatureSummary.RunEndpointID), signatureSummary.Timestamp, shortDiagnosticValue(signatureSummary.Nonce), shortDiagnosticValue(signatureSummary.BodyHash), shortDiagnosticValue(signatureSummary.Signature))
|
||||
|
||||
httpResponse, err := client.httpClient.Do(httpRequest)
|
||||
if err != nil {
|
||||
log.Printf("RUN platform request status=send_error method=POST base=%s path=%s durationMs=%d error=%s", diagnosticLogValue(client.baseURL), path, time.Since(startedAt).Milliseconds(), err)
|
||||
return response, fmt.Errorf("send platform request: %w", err)
|
||||
}
|
||||
defer httpResponse.Body.Close()
|
||||
log.Printf("RUN platform request status=response method=POST base=%s path=%s httpStatus=%d durationMs=%d", diagnosticLogValue(client.baseURL), path, httpResponse.StatusCode, time.Since(startedAt).Milliseconds())
|
||||
|
||||
if httpResponse.StatusCode < http.StatusOK || httpResponse.StatusCode >= http.StatusMultipleChoices {
|
||||
var failure struct {
|
||||
Code string `json:"code"`
|
||||
Details []string `json:"details"`
|
||||
}
|
||||
_ = json.NewDecoder(io.LimitReader(httpResponse.Body, 64<<10)).Decode(&failure)
|
||||
return response, PlatformRequestError{Status: httpResponse.StatusCode, Path: path, Code: failure.Code, Details: failure.Details}
|
||||
}
|
||||
if err := json.NewDecoder(httpResponse.Body).Decode(&response); err != nil {
|
||||
return response, fmt.Errorf("decode platform response: %w", err)
|
||||
}
|
||||
client.observeResponseServerTime(response)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func postPlatformJSON[Request any, Response any](ctx context.Context, client PlatformClient, path string, request Request) (Response, error) {
|
||||
var response Response
|
||||
startedAt := time.Now()
|
||||
@@ -405,6 +465,10 @@ func signRunRequest(request *http.Request, body []byte, stamp time.Time) (runReq
|
||||
if err := json.Unmarshal(body, &envelope); err != nil {
|
||||
return runRequestSignatureSummary{}, fmt.Errorf("decode Run signing envelope: %w", err)
|
||||
}
|
||||
return signRunRequestWithEnvelope(request, body, envelope, stamp)
|
||||
}
|
||||
|
||||
func signRunRequestWithEnvelope(request *http.Request, body []byte, envelope runRequestEnvelope, stamp time.Time) (runRequestSignatureSummary, error) {
|
||||
if strings.TrimSpace(envelope.RunEndpointID) == "" || strings.TrimSpace(envelope.SessionToken) == "" {
|
||||
return runRequestSignatureSummary{}, fmt.Errorf("Run signing envelope requires endpoint and session token")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user