Files
ThothII/tools/tht/internal/backup/preflight_test.go
T

703 lines
24 KiB
Go

package backup
import (
"archive/tar"
"archive/zip"
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/aritmolab/thothii/tools/tht/internal/config"
)
func TestPreflightReturnsValidatedMetadataAndCallsAllTargetChecksWithoutExtracting(t *testing.T) {
installation := preflightTestInstallation(t)
archive := filepath.Join(t.TempDir(), "valid.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("safe")}},
})
checks := make([]string, 0, 3)
result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, PreflightDependencies{
FreeBytes: func(string) (uint64, error) { return 1024, nil },
CheckOwnershipPermissions: func(context.Context, config.Installation, Manifest) error {
checks = append(checks, "ownership")
return nil
},
CheckVolumeMapping: func(context.Context, config.Installation, Manifest) error {
checks = append(checks, "volumes")
return nil
},
CheckImageConfigCompatibility: func(context.Context, config.Installation, Manifest) error {
checks = append(checks, "images")
return nil
},
})
if err != nil {
t.Fatal(err)
}
if result.Manifest.InstallationID != "local-dev" || result.Manifest.Entries[0].Path != "configuration/operator.env" {
t.Fatalf("validated metadata = %#v", result)
}
if result.ArchivePath != archive || result.RequiredBytes != 4 || len(result.Entries) != 1 {
t.Fatalf("archive metadata = %#v", result)
}
if strings.Join(checks, ",") != "ownership,volumes,images" {
t.Fatalf("target checks = %v", checks)
}
if _, err := os.Stat(filepath.Join(installation.ProjectDirectory, "configuration", "operator.env")); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("preflight extracted into a final target: %v", err)
}
}
func TestPreflightStagesOnTheAccountedInstallationFilesystemInsteadOfTMPDIR(t *testing.T) {
installation := preflightTestInstallation(t)
if err := os.MkdirAll(installation.ControlDirectory(), 0o700); err != nil {
t.Fatal(err)
}
archive := filepath.Join(t.TempDir(), "valid.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("safe")}},
})
blockedTMPDIR := filepath.Join(t.TempDir(), "not-a-directory")
if err := os.WriteFile(blockedTMPDIR, []byte("blocked"), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("TMPDIR", blockedTMPDIR)
var capacityTargets []string
dependencies := permissivePreflightDependencies()
dependencies.FreeBytes = func(target string) (uint64, error) {
capacityTargets = append(capacityTargets, target)
return 1 << 30, nil
}
result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, dependencies)
if err != nil {
t.Fatal(err)
}
defer result.CloseArchive()
staged, err := result.StageArchive(context.Background())
if err != nil {
t.Fatal(err)
}
defer staged.Close()
if len(capacityTargets) == 0 || capacityTargets[0] != installation.ControlDirectory() {
t.Fatalf("free-space targets = %q, want installation control directory %q", capacityTargets, installation.ControlDirectory())
}
stagingRoot := filepath.Join(installation.ControlDirectory(), "restore-staging")
if relative, err := filepath.Rel(stagingRoot, staged.directory); err != nil || relative == "." || strings.HasPrefix(relative, "..") {
t.Fatalf("staging directory = %q, want a child of %q", staged.directory, stagingRoot)
}
}
func TestPreflightRejectsAdversarialArchiveEntriesAndManifestIdentity(t *testing.T) {
installation := preflightTestInstallation(t)
tests := []struct {
name string
make func(string)
}{
{
name: "traversal",
make: func(path string) {
writePreflightArchive(t, path, preflightArchiveSpec{rawEntries: []preflightRawArchiveEntry{{path: "../outside", body: []byte("x")}}})
},
},
{
name: "absolute",
make: func(path string) {
writePreflightArchive(t, path, preflightArchiveSpec{rawEntries: []preflightRawArchiveEntry{{path: "/outside", body: []byte("x")}}})
},
},
{
name: "windows absolute",
make: func(path string) {
writePreflightArchive(t, path, preflightArchiveSpec{rawEntries: []preflightRawArchiveEntry{{path: `C:\outside`, body: []byte("x")}}})
},
},
{
name: "normalized traversal",
make: func(path string) {
writePreflightArchive(t, path, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "target", body: []byte("x")}},
rawManifest: []byte(`{"schema_version":1,"installation_id":"local-dev","created_at":"2026-08-16T10:00:00Z","source_revision":"` + testRevision + `","includes_secrets":false,"compose_project":"thothii-test","images":[],"volumes":[],"entries":[{"path":"foo/../target","kind":"file","owner":"installation","sha256":"` + digestForPreflight([]byte("x")) + `","size":1,"archived":true}]}`),
})
},
},
{
name: "symlink",
make: func(path string) {
writePreflightArchive(t, path, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "link", body: []byte("target"), symlink: true}},
})
},
},
{
name: "duplicate",
make: func(path string) {
writePreflightArchive(t, path, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "file", body: []byte("one")}},
rawEntries: []preflightRawArchiveEntry{{path: "file", body: []byte("two")}},
})
},
},
{
name: "checksum mismatch",
make: func(path string) {
writePreflightArchive(t, path, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "file", body: []byte("actual"), checksum: "sha256:" + strings.Repeat("0", 64)}},
})
},
},
{
name: "unknown schema",
make: func(path string) {
writePreflightArchive(t, path, preflightArchiveSpec{schemaVersion: CurrentSchemaVersion + 1})
},
},
{
name: "wrong installation",
make: func(path string) {
writePreflightArchive(t, path, preflightArchiveSpec{installationID: "another-installation"})
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
archive := filepath.Join(t.TempDir(), test.name+".zip")
test.make(archive)
_, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies())
if err == nil {
t.Fatal("Preflight accepted adversarial archive")
}
})
}
}
func TestPreflightRequiresExplicitProtectionForExternalSecretPayloadsWithoutLeakingIt(t *testing.T) {
installation := preflightTestInstallation(t)
secret := []byte("never-print-this-secret")
archive := filepath.Join(t.TempDir(), "secrets.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{
includeSecrets: true,
entries: []preflightArchiveEntry{{path: "external-secrets/000", body: secret, kind: EntryExternalSecret, sensitive: true}},
})
_, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: false}, permissivePreflightDependencies())
if err == nil || strings.Contains(err.Error(), string(secret)) || !strings.Contains(err.Error(), "confirmation") {
t.Fatalf("secret policy error = %v", err)
}
_, err = Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true, AllowExternalSecrets: true}, permissivePreflightDependencies())
if err != nil {
t.Fatal(err)
}
}
func TestPreflightRejectsInsufficientDiskAndEachTargetCompatibilityFailure(t *testing.T) {
installation := preflightTestInstallation(t)
archive := filepath.Join(t.TempDir(), "checked.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{entries: []preflightArchiveEntry{{path: "large", body: []byte("123456789")}}})
tests := []struct {
name string
deps func(error) PreflightDependencies
want string
}{
{name: "disk", deps: func(error) PreflightDependencies {
deps := permissivePreflightDependencies()
deps.FreeBytes = func(string) (uint64, error) { return 1, nil }
return deps
}, want: "free disk"},
{name: "ownership", deps: func(want error) PreflightDependencies {
deps := permissivePreflightDependencies()
deps.CheckOwnershipPermissions = func(context.Context, config.Installation, Manifest) error { return want }
return deps
}, want: "ownership"},
{name: "volume mapping", deps: func(want error) PreflightDependencies {
deps := permissivePreflightDependencies()
deps.CheckVolumeMapping = func(context.Context, config.Installation, Manifest) error { return want }
return deps
}, want: "volume mapping"},
{name: "image config", deps: func(want error) PreflightDependencies {
deps := permissivePreflightDependencies()
deps.CheckImageConfigCompatibility = func(context.Context, config.Installation, Manifest) error { return want }
return deps
}, want: "image config"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
want := errors.New(test.want + " rejected")
_, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, test.deps(want))
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("Preflight() error = %v, want %q", err, test.want)
}
})
}
}
func TestPreflightRejectsTraversalAndSymlinkInsideVolumeTar(t *testing.T) {
installation := preflightTestInstallation(t)
for _, test := range []struct {
name string
header tar.Header
}{
{name: "traversal", header: tar.Header{Name: "../outside", Mode: 0o600, Size: 1, Typeflag: tar.TypeReg}},
{name: "symlink", header: tar.Header{Name: "link", Mode: 0o777, Typeflag: tar.TypeSymlink, Linkname: "../../outside"}},
} {
t.Run(test.name, func(t *testing.T) {
var payload strings.Builder
writer := tar.NewWriter(&stringWriter{value: &payload})
if err := writer.WriteHeader(&test.header); err != nil {
t.Fatal(err)
}
if test.header.Size > 0 {
if _, err := writer.Write([]byte("x")); err != nil {
t.Fatal(err)
}
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
archive := filepath.Join(t.TempDir(), test.name+".zip")
writePreflightArchive(t, archive, preflightArchiveSpec{entries: []preflightArchiveEntry{{path: "volumes/sessions.tar", body: []byte(payload.String()), kind: EntryVolume}}})
if _, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies()); err == nil {
t.Fatal("Preflight accepted unsafe TAR member")
}
})
}
}
func TestPreflightAcceptsCanonicalTarRootDirectoryAndRelativeMembers(t *testing.T) {
installation := preflightTestInstallation(t)
var payload strings.Builder
writer := tar.NewWriter(&stringWriter{value: &payload})
for _, header := range []tar.Header{
{Name: "./", Mode: 0o755, Typeflag: tar.TypeDir},
{Name: "./payload", Mode: 0o600, Size: 1, Typeflag: tar.TypeReg},
} {
if err := writer.WriteHeader(&header); err != nil {
t.Fatal(err)
}
if header.Size > 0 {
if _, err := writer.Write([]byte("x")); err != nil {
t.Fatal(err)
}
}
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
archive := filepath.Join(t.TempDir(), "canonical-volume.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{entries: []preflightArchiveEntry{{
path: "volumes/sessions.tar", body: []byte(payload.String()), kind: EntryVolume,
}}})
result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies())
if err != nil {
t.Fatal(err)
}
result.CloseArchive()
}
func TestPreflightRejectsNonDirectoryTarRootMarker(t *testing.T) {
installation := preflightTestInstallation(t)
var payload strings.Builder
writer := tar.NewWriter(&stringWriter{value: &payload})
header := tar.Header{Name: ".", Mode: 0o600, Size: 1, Typeflag: tar.TypeReg}
if err := writer.WriteHeader(&header); err != nil {
t.Fatal(err)
}
if _, err := writer.Write([]byte("x")); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
archive := filepath.Join(t.TempDir(), "unsafe-root-volume.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{entries: []preflightArchiveEntry{{
path: "volumes/sessions.tar", body: []byte(payload.String()), kind: EntryVolume,
}}})
if _, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies()); err == nil {
t.Fatal("Preflight accepted a non-directory TAR root marker")
}
}
func TestPreflightRejectsArchivesThatExceedConfiguredProcessingLimits(t *testing.T) {
installation := preflightTestInstallation(t)
tests := []struct {
name string
spec preflightArchiveSpec
limits PreflightLimits
wantErr string
}{
{
name: "member count",
spec: preflightArchiveSpec{entries: []preflightArchiveEntry{{path: "one", body: []byte("one")}}},
limits: PreflightLimits{MaxMembers: 1, MaxUncompressedBytes: 1 << 20, MaxCompressionRatio: 100},
wantErr: "member limit",
},
{
name: "uncompressed bytes",
spec: preflightArchiveSpec{entries: []preflightArchiveEntry{{path: "large", body: []byte("123456789")}}},
limits: PreflightLimits{MaxMembers: 10, MaxUncompressedBytes: 1, MaxCompressionRatio: 100},
wantErr: "uncompressed-size limit",
},
{
name: "compression ratio",
spec: preflightArchiveSpec{entries: []preflightArchiveEntry{{
path: "compressed", body: []byte(strings.Repeat("A", 4096)), method: zip.Deflate,
}}},
limits: PreflightLimits{MaxMembers: 10, MaxUncompressedBytes: 1 << 20, MaxCompressionRatio: 2},
wantErr: "compression-ratio limit",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
archive := filepath.Join(t.TempDir(), "limited.zip")
writePreflightArchive(t, archive, test.spec)
_, err := Preflight(context.Background(), installation, PreflightRequest{
Archive: archive, Confirm: true, Limits: test.limits,
}, permissivePreflightDependencies())
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("Preflight() error = %v, want %q", err, test.wantErr)
}
})
}
}
func TestPreflightRevalidationRefusesAnArchivePathThatWasReplaced(t *testing.T) {
installation := preflightTestInstallation(t)
archive := filepath.Join(t.TempDir(), "checked.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("validated")}},
})
result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies())
if err != nil {
t.Fatal(err)
}
defer result.CloseArchive()
replacement := filepath.Join(t.TempDir(), "replacement.zip")
writePreflightArchive(t, replacement, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("unchecked")}},
})
if err := os.Rename(replacement, archive); err != nil {
t.Fatal(err)
}
if _, err := result.RevalidateArchive(); err == nil || !strings.Contains(err.Error(), "changed") {
t.Fatalf("RevalidateArchive() error = %v, want replaced path refusal", err)
}
}
func TestPreflightStagesArchiveIntoImmutablePrivateBytes(t *testing.T) {
installation := preflightTestInstallation(t)
if err := os.MkdirAll(installation.ControlDirectory(), 0o700); err != nil {
t.Fatal(err)
}
archive := filepath.Join(t.TempDir(), "checked.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("before")}},
})
result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies())
if err != nil {
t.Fatal(err)
}
defer result.CloseArchive()
staged, err := result.StageArchive(context.Background())
if err != nil {
t.Fatal(err)
}
defer staged.Close()
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("after!")}},
})
reader, err := zip.NewReader(staged.file, result.ArchiveSize)
if err != nil {
t.Fatal(err)
}
if len(reader.File) < 2 {
t.Fatalf("staged archive members = %d, want manifest and payload", len(reader.File))
}
var payload *zip.File
for _, member := range reader.File {
if member.Name == "configuration/operator.env" {
payload = member
break
}
}
if payload == nil {
t.Fatal("staged archive is missing the configured payload")
}
stream, err := payload.Open()
if err != nil {
t.Fatal(err)
}
defer stream.Close()
body, err := io.ReadAll(stream)
if err != nil {
t.Fatal(err)
}
if string(body) != "before" {
t.Fatalf("staged payload = %q, want preflighted bytes", body)
}
}
func TestStageArchiveCleansPrivateFileAfterStreamingFailure(t *testing.T) {
installation := preflightTestInstallation(t)
if err := os.MkdirAll(installation.ControlDirectory(), 0o700); err != nil {
t.Fatal(err)
}
archive := filepath.Join(t.TempDir(), "checked.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("before")}},
})
result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies())
if err != nil {
t.Fatal(err)
}
defer result.CloseArchive()
result.freeBytes = func(string) (uint64, error) {
result.archive.digest = "after-preflight-mismatch"
return 1024, nil
}
if _, err := result.StageArchive(context.Background()); err == nil || !strings.Contains(err.Error(), "changed") {
t.Fatalf("StageArchive() error = %v, want streaming size refusal", err)
}
entries, err := os.ReadDir(result.stagingRoot)
if err != nil {
t.Fatal(err)
}
if len(entries) != 0 {
t.Fatalf("private staging leftovers = %v, want none", entries)
}
}
func TestPreflightStagingRejectsInPlaceArchiveHashMutation(t *testing.T) {
installation := preflightTestInstallation(t)
archive := filepath.Join(t.TempDir(), "checked.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("before")}},
})
result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies())
if err != nil {
t.Fatal(err)
}
defer result.CloseArchive()
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("after!")}},
})
if _, err := result.StageArchive(context.Background()); err == nil || !strings.Contains(err.Error(), "changed") {
t.Fatalf("StageArchive() error = %v, want changed archive refusal", err)
}
}
func TestPreflightRejectsArchiveEntryWithModeDifferentFromManifest(t *testing.T) {
installation := preflightTestInstallation(t)
archive := filepath.Join(t.TempDir(), "mode-mismatch.zip")
manifestMode := uint32(0o600)
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{
path: "configuration/operator.env", body: []byte("safe"), mode: 0o644, manifestMode: &manifestMode,
}},
})
_, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies())
if err == nil || !strings.Contains(err.Error(), "mode mismatch") {
t.Fatalf("Preflight() error = %v, want mode mismatch", err)
}
}
type preflightArchiveSpec struct {
installationID string
schemaVersion int
includeSecrets bool
entries []preflightArchiveEntry
rawEntries []preflightRawArchiveEntry
rawManifest []byte
volumes []VolumeMetadata
}
type preflightArchiveEntry struct {
path string
body []byte
checksum string
kind string
sensitive bool
symlink bool
mode os.FileMode
manifestMode *uint32
method uint16
owner string
logicalName string
sourcePath string
}
type preflightRawArchiveEntry struct {
path string
body []byte
}
func preflightTestInstallation(t *testing.T) config.Installation {
t.Helper()
root, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal(err)
}
return config.Installation{
Path: filepath.Join(root, "deploy", "local-dev", "thothii-installation.yaml"),
ProjectDirectory: root,
}
}
func permissivePreflightDependencies() PreflightDependencies {
return PreflightDependencies{
FreeBytes: func(string) (uint64, error) { return 1 << 30, nil },
CheckOwnershipPermissions: func(context.Context, config.Installation, Manifest) error { return nil },
CheckVolumeMapping: func(context.Context, config.Installation, Manifest) error { return nil },
CheckImageConfigCompatibility: func(context.Context, config.Installation, Manifest) error { return nil },
}
}
func writePreflightArchive(t *testing.T, archivePath string, spec preflightArchiveSpec) {
t.Helper()
if spec.installationID == "" {
spec.installationID = "local-dev"
}
if spec.schemaVersion == 0 {
spec.schemaVersion = CurrentSchemaVersion
}
manifest := Manifest{
SchemaVersion: spec.schemaVersion, InstallationID: spec.installationID,
CreatedAt: time.Date(2026, 8, 16, 10, 0, 0, 0, time.UTC), SourceRevision: testRevision,
IncludesSecrets: spec.includeSecrets, ComposeProject: "thothii-test",
Volumes: append([]VolumeMetadata(nil), spec.volumes...),
}
for _, entry := range spec.entries {
checksum := entry.checksum
if checksum == "" {
checksum = digestForPreflight(entry.body)
}
kind := entry.kind
if kind == "" {
kind = EntryFile
}
sourcePath := entry.sourcePath
owner := entry.owner
if owner == "" {
owner = "installation"
}
if kind == EntryExternalSecret {
if sourcePath == "" {
sourcePath = "/protected/secret"
}
if entry.owner == "" {
owner = "external-secret"
}
}
mode := uint32(entry.mode.Perm())
if mode == 0 {
mode = 0o600
}
if entry.manifestMode != nil {
mode = *entry.manifestMode
}
manifest.Entries = append(manifest.Entries, Entry{Path: entry.path, Kind: kind, Owner: owner, LogicalName: entry.logicalName, SourcePath: sourcePath, SHA256: checksum, Size: int64(len(entry.body)), Mode: mode, Archived: true, Sensitive: entry.sensitive})
}
manifestBytes, err := manifest.JSON()
if err != nil {
if spec.schemaVersion != CurrentSchemaVersion || spec.installationID != "local-dev" {
manifestBytes = rawPreflightManifest(spec)
} else {
t.Fatal(err)
}
}
if spec.rawManifest != nil {
manifestBytes = spec.rawManifest
}
file, err := os.Create(archivePath)
if err != nil {
t.Fatal(err)
}
defer file.Close()
writer := zip.NewWriter(file)
for _, entry := range spec.entries {
method := entry.method
if method == 0 {
method = zip.Store
}
header := &zip.FileHeader{Name: entry.path, Method: method}
mode := entry.mode
if mode == 0 {
mode = 0o600
}
header.SetMode(mode)
if entry.symlink {
header.SetMode(os.ModeSymlink | 0o777)
}
created, err := writer.CreateHeader(header)
if err != nil {
t.Fatal(err)
}
if _, err := created.Write(entry.body); err != nil {
t.Fatal(err)
}
}
for _, entry := range spec.rawEntries {
created, err := writer.CreateHeader(&zip.FileHeader{Name: entry.path, Method: zip.Store})
if err != nil {
t.Fatal(err)
}
if _, err := created.Write(entry.body); err != nil {
t.Fatal(err)
}
}
manifestHeader := &zip.FileHeader{Name: ManifestPath, Method: zip.Store}
manifestHeader.SetMode(0o600)
created, err := writer.CreateHeader(manifestHeader)
if err != nil {
t.Fatal(err)
}
if _, err := created.Write(manifestBytes); err != nil {
t.Fatal(err)
}
if err := writer.Close(); err != nil {
t.Fatal(err)
}
if err := file.Close(); err != nil {
t.Fatal(err)
}
}
func rawPreflightManifest(spec preflightArchiveSpec) []byte {
return []byte(`{"schema_version":999,"installation_id":"` + spec.installationID + `","created_at":"2026-08-16T10:00:00Z","source_revision":"` + testRevision + `","includes_secrets":false,"compose_project":"thothii-test","images":[],"volumes":[],"entries":[]}`)
}
func digestForPreflight(value []byte) string {
digest := sha256.Sum256(value)
return "sha256:" + hex.EncodeToString(digest[:])
}
type stringWriter struct {
value *strings.Builder
}
func (writer *stringWriter) Write(value []byte) (int, error) {
return writer.value.Write(value)
}