init
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"browser.local/run/protocol"
|
||||
)
|
||||
|
||||
func TestRunSelfUpdateResumesPartialDownloadAndRejectsChecksum(t *testing.T) {
|
||||
assignment, input, payload := selfUpdateTestFixture(t)
|
||||
client := newFakeWorkerClient()
|
||||
client.updateInput = input
|
||||
client.updatePayload = payload
|
||||
cfg := workerTestConfig(t)
|
||||
transactionRoot := filepath.Join(cfg.WorkspaceRoot, "self-updates", safeWorkspaceName(assignment.JobID))
|
||||
if err := os.MkdirAll(transactionRoot, 0o700); err != nil {
|
||||
t.Fatalf("create transaction root: %v", err)
|
||||
}
|
||||
partial := len(payload) / 3
|
||||
if err := os.WriteFile(filepath.Join(transactionRoot, "update.archive"), payload[:partial], 0o600); err != nil {
|
||||
t.Fatalf("write partial update: %v", err)
|
||||
}
|
||||
worker, err := NewWorker(cfg, client, WithSelfUpdateActivator(&recordingSelfUpdateActivator{}))
|
||||
if err != nil {
|
||||
t.Fatalf("new worker: %v", err)
|
||||
}
|
||||
worker.state.SessionToken = "session-token"
|
||||
result := worker.executeRunSelfUpdate(context.Background(), assignment)
|
||||
if result.State != lifecycleResultStateSucceeded || result.ExecutionResult.Kind != "run.update.staged" || result.ActivationManifest == "" {
|
||||
t.Fatalf("expected staged self-update, got %+v", result)
|
||||
}
|
||||
if len(client.updateChunkOffsets) == 0 || client.updateChunkOffsets[0] != int64(partial) {
|
||||
t.Fatalf("expected resumable range from %d, got %+v", partial, client.updateChunkOffsets)
|
||||
}
|
||||
|
||||
badClient := newFakeWorkerClient()
|
||||
badInput := input
|
||||
badInput.Checksum = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
badClient.updateInput = badInput
|
||||
badClient.updatePayload = payload
|
||||
badWorker, err := NewWorker(workerTestConfig(t), badClient, WithSelfUpdateActivator(&recordingSelfUpdateActivator{}))
|
||||
if err != nil {
|
||||
t.Fatalf("new bad checksum worker: %v", err)
|
||||
}
|
||||
badWorker.state.SessionToken = "session-token"
|
||||
bad := badWorker.executeRunSelfUpdate(context.Background(), assignment)
|
||||
if bad.State != lifecycleResultStateFailed || bad.ErrorCode != "self_update_download_failed" {
|
||||
t.Fatalf("expected final checksum rejection, got %+v", bad)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelfUpdateActivationWaitsForAcceptedTerminalResult(t *testing.T) {
|
||||
assignment, input, payload := selfUpdateTestFixture(t)
|
||||
client := newFakeWorkerClient()
|
||||
client.claimJob = assignment
|
||||
client.updateInput = input
|
||||
client.updatePayload = payload
|
||||
client.resultErr = errors.New("stale lease")
|
||||
activator := &recordingSelfUpdateActivator{}
|
||||
worker, err := NewWorker(workerTestConfig(t), client, WithSelfUpdateActivator(activator))
|
||||
if err != nil {
|
||||
t.Fatalf("new worker: %v", err)
|
||||
}
|
||||
if err := worker.Register(context.Background()); err != nil {
|
||||
t.Fatalf("register: %v", err)
|
||||
}
|
||||
handled, err := worker.ClaimAndRunOnce(context.Background())
|
||||
if !handled || err == nil {
|
||||
t.Fatalf("expected rejected result error, handled=%v err=%v", handled, err)
|
||||
}
|
||||
if activator.manifestPath != "" {
|
||||
t.Fatalf("stale terminal result must not activate update: %s", activator.manifestPath)
|
||||
}
|
||||
if worker.journal.ActiveCount() != 1 {
|
||||
t.Fatal("staged result must remain recoverable until Platform accepts it")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelfUpdateReplacementPreservesRollbackAndRejectsTraversal(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
current := filepath.Join(root, "run")
|
||||
staged := filepath.Join(root, "staged-run")
|
||||
backup := filepath.Join(root, "previous-run")
|
||||
if err := os.WriteFile(current, []byte("old-run"), 0o700); err != nil {
|
||||
t.Fatalf("write current: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(staged, []byte("new-run"), 0o700); err != nil {
|
||||
t.Fatalf("write staged: %v", err)
|
||||
}
|
||||
checksum, _, err := checksumFile(staged)
|
||||
if err != nil {
|
||||
t.Fatalf("checksum staged: %v", err)
|
||||
}
|
||||
manifest := SelfUpdateManifest{StagedExecutable: staged, CurrentExecutable: current, BackupExecutable: backup, BinaryChecksum: checksum}
|
||||
if err := replaceRunExecutable(manifest); err != nil {
|
||||
t.Fatalf("replace executable: %v", err)
|
||||
}
|
||||
if body, _ := os.ReadFile(current); string(body) != "new-run" {
|
||||
t.Fatalf("expected new executable, got %q", body)
|
||||
}
|
||||
if err := rollbackRunExecutable(manifest); err != nil {
|
||||
t.Fatalf("rollback executable: %v", err)
|
||||
}
|
||||
if body, _ := os.ReadFile(current); string(body) != "old-run" {
|
||||
t.Fatalf("expected previous executable after rollback, got %q", body)
|
||||
}
|
||||
|
||||
archivePath := filepath.Join(root, "unsafe.tar.gz")
|
||||
var archive bytes.Buffer
|
||||
gzipWriter := gzip.NewWriter(&archive)
|
||||
tarWriter := tar.NewWriter(gzipWriter)
|
||||
body := []byte("escape")
|
||||
if err := tarWriter.WriteHeader(&tar.Header{Name: "../run", Mode: 0o700, Size: int64(len(body)), Typeflag: tar.TypeReg}); err != nil {
|
||||
t.Fatalf("write unsafe header: %v", err)
|
||||
}
|
||||
_, _ = tarWriter.Write(body)
|
||||
_ = tarWriter.Close()
|
||||
_ = gzipWriter.Close()
|
||||
if err := os.WriteFile(archivePath, archive.Bytes(), 0o600); err != nil {
|
||||
t.Fatalf("write unsafe archive: %v", err)
|
||||
}
|
||||
if _, err := extractRunUpdateBinary(archivePath, "tar.gz", "run", filepath.Join(root, "escaped")); err == nil || !strings.Contains(err.Error(), "unsafe") {
|
||||
t.Fatalf("expected traversal rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareSelfUpdateManifestRevalidatesStagedBinaryAfterRestart(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
assignment := workerJobAssignment(protocol.RunCapabilityRunSelfUpdate)
|
||||
assignment.TargetKey = "run/update"
|
||||
assignment.InputRef = "artifact://artifact-run-staged"
|
||||
input := protocol.RunUpdateInputResponse{JobID: assignment.JobID, ServerInstanceID: assignment.ServerInstanceID, RunEndpointID: assignment.RunEndpointID, ArtifactID: "artifact-run-staged", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 16, TargetOS: runtime.GOOS, TargetArch: runtime.GOARCH, PackageFormat: "tar.gz", ExecutableName: "run", TargetRelease: "release-staged", ChunkSizeBytes: 8}
|
||||
manifestPath := filepath.Join(root, "manifest.json")
|
||||
stagedPath := filepath.Join(root, "run.staged")
|
||||
if err := os.WriteFile(stagedPath, []byte("staged-binary"), 0o700); err != nil {
|
||||
t.Fatalf("write staged binary: %v", err)
|
||||
}
|
||||
checksum, _, err := checksumFile(stagedPath)
|
||||
if err != nil {
|
||||
t.Fatalf("checksum staged binary: %v", err)
|
||||
}
|
||||
manifest := SelfUpdateManifest{Version: selfUpdateManifestVersion, JobID: assignment.JobID, Attempt: assignment.Attempt, LeaseToken: assignment.LeaseToken, ArtifactID: input.ArtifactID, ArtifactChecksum: input.Checksum, ArtifactSizeBytes: input.SizeBytes, TargetOS: input.TargetOS, TargetArch: input.TargetArch, TargetRelease: input.TargetRelease, StagedExecutable: stagedPath, BinaryChecksum: checksum, Phase: "staged", CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC()}
|
||||
if err := persistSelfUpdateManifest(manifestPath, manifest); err != nil {
|
||||
t.Fatalf("persist staged manifest: %v", err)
|
||||
}
|
||||
if _, err := prepareSelfUpdateManifest(manifestPath, assignment, input, root); err != nil {
|
||||
t.Fatalf("revalidate staged manifest: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(stagedPath, []byte("tampered-binary"), 0o700); err != nil {
|
||||
t.Fatalf("tamper staged binary: %v", err)
|
||||
}
|
||||
if _, err := prepareSelfUpdateManifest(manifestPath, assignment, input, root); err == nil || !strings.Contains(err.Error(), "checksum changed") {
|
||||
t.Fatalf("expected staged checksum rejection, got %v", err)
|
||||
}
|
||||
manifest.StagedExecutable = filepath.Join(root, "..", "outside")
|
||||
if err := persistSelfUpdateManifest(manifestPath, manifest); err != nil {
|
||||
t.Fatalf("persist unsafe staged manifest: %v", err)
|
||||
}
|
||||
if _, err := prepareSelfUpdateManifest(manifestPath, assignment, input, root); err == nil || !strings.Contains(err.Error(), "outside") {
|
||||
t.Fatalf("expected staged path rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func selfUpdateTestFixture(t *testing.T) (protocol.RunJobAssignment, protocol.RunUpdateInputResponse, []byte) {
|
||||
t.Helper()
|
||||
assignment := workerJobAssignment(protocol.RunCapabilityRunSelfUpdate)
|
||||
assignment.TargetKey = "run/update"
|
||||
assignment.InputRef = "artifact://artifact-run-latest"
|
||||
assignment.State = "running"
|
||||
assignment.LeaseToken = "lease-update"
|
||||
assignment.Attempt = 1
|
||||
executableName := "run"
|
||||
if runtime.GOOS == "windows" {
|
||||
executableName = "run.exe"
|
||||
}
|
||||
payload := []byte("self-update-test-binary")
|
||||
input := protocol.RunUpdateInputResponse{JobID: assignment.JobID, ServerInstanceID: assignment.ServerInstanceID, RunEndpointID: assignment.RunEndpointID, ArtifactID: "artifact-run-latest", Checksum: bytesChecksum(payload), SizeBytes: int64(len(payload)), TargetOS: runtime.GOOS, TargetArch: runtime.GOARCH, PackageFormat: "raw-executable", ExecutableName: executableName, TargetRelease: "run-release-test", ChunkSizeBytes: 64}
|
||||
return assignment, input, payload
|
||||
}
|
||||
Reference in New Issue
Block a user