Binary catalog install/update paths (installBinary/replaceBinary) saved raw plugin binaries into plugin_archives.archive_bytes, but startup preload opens that column with zip.NewReader expecting a package containing manifest.json and the plugin binary. Affected installs failed preload after restart with "zip: not a valid zip file". - Package manifest.json + binary into a validated zip before SaveArchive on both binary install paths. - Self-repair legacy rows in ArchiveCache.Ensure: when stored bytes are not a zip but hash-match both the stored checksum and the stored manifest's checksum, repackage and persist the repaired archive. A persist failure is logged, not fatal — the in-memory archive is already valid and recovery retries on the next preload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
235 lines
7.3 KiB
Go
235 lines
7.3 KiB
Go
package plugins
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
|
|
pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1"
|
|
)
|
|
|
|
func TestInstallerInstallBinaryPersistsPackagedArchive(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
store := newRecordingInstallationStore()
|
|
installer := NewInstaller(store, InstallerOptions{BaseDir: t.TempDir()})
|
|
|
|
manifest := testPluginManifest(t, "silo.metadb", "0.0.19")
|
|
binaryData := []byte("#!/bin/sh\nexit 0\n")
|
|
checksum := sha256.Sum256(binaryData)
|
|
manifest.Checksum = hex.EncodeToString(checksum[:])
|
|
|
|
result, err := installer.installBinary(ctx, binaryData, manifest.GetChecksum(), manifest, nil)
|
|
if err != nil {
|
|
t.Fatalf("installBinary() returned error: %v", err)
|
|
}
|
|
|
|
if result.Installation.ID != 1 {
|
|
t.Fatalf("result installation id = %d, want 1", result.Installation.ID)
|
|
}
|
|
if len(store.saveArchiveIDs) != 1 || store.saveArchiveIDs[0] != 1 {
|
|
t.Fatalf("SaveArchive() ids = %#v, want [1]", store.saveArchiveIDs)
|
|
}
|
|
assertStoredPluginArchive(t, store.saveArchiveBytes, store.saveArchiveManifest, binaryData)
|
|
}
|
|
|
|
func TestInstallerReplaceBinaryPreservesInstallationID(t *testing.T) {
|
|
ctx := context.Background()
|
|
|
|
oldDir := t.TempDir()
|
|
oldPath := filepath.Join(oldDir, "plugin")
|
|
if err := os.WriteFile(oldPath, []byte("#!/bin/sh\nexit 0\n"), 0755); err != nil {
|
|
t.Fatalf("WriteFile(%q) returned error: %v", oldPath, err)
|
|
}
|
|
|
|
store := newRecordingInstallationStore()
|
|
installer := NewInstaller(store, InstallerOptions{BaseDir: t.TempDir()})
|
|
|
|
manifest := testPluginManifest(t, "silo.metadb", "0.0.19")
|
|
binaryData := []byte("#!/bin/sh\nexit 0\n")
|
|
checksum := sha256.Sum256(binaryData)
|
|
manifest.Checksum = hex.EncodeToString(checksum[:])
|
|
|
|
result, err := installer.replaceBinary(ctx, &Installation{
|
|
ID: 15,
|
|
PluginID: "silo.metadb",
|
|
Version: "0.0.18",
|
|
InstallPath: oldPath,
|
|
Enabled: true,
|
|
}, binaryData, hex.EncodeToString(checksum[:]), manifest)
|
|
if err != nil {
|
|
t.Fatalf("replaceBinary() returned error: %v", err)
|
|
}
|
|
|
|
if len(store.createInputs) != 0 {
|
|
t.Fatalf("Create() called %d times, want 0", len(store.createInputs))
|
|
}
|
|
if len(store.saveArchiveIDs) != 1 || store.saveArchiveIDs[0] != 15 {
|
|
t.Fatalf("SaveArchive() ids = %#v, want [15]", store.saveArchiveIDs)
|
|
}
|
|
assertStoredPluginArchive(t, store.saveArchiveBytes, store.saveArchiveManifest, binaryData)
|
|
if len(store.updateIDs) != 1 || store.updateIDs[0] != 15 {
|
|
t.Fatalf("Update() ids = %#v, want [15]", store.updateIDs)
|
|
}
|
|
if result.Installation.ID != 15 {
|
|
t.Fatalf("result installation id = %d, want 15", result.Installation.ID)
|
|
}
|
|
if result.Installation.Version != "0.0.19" {
|
|
t.Fatalf("result installation version = %q, want 0.0.19", result.Installation.Version)
|
|
}
|
|
if result.Installation.InstallPath == oldPath {
|
|
t.Fatal("expected replaceBinary() to move the installation to a new path")
|
|
}
|
|
if result.Installation.InstallPath != result.BinaryPath {
|
|
t.Fatalf("result install path = %q, want binary path %q", result.Installation.InstallPath, result.BinaryPath)
|
|
}
|
|
if _, err := os.Stat(oldDir); !os.IsNotExist(err) {
|
|
t.Fatalf("expected old installation dir to be removed, stat error = %v", err)
|
|
}
|
|
if _, err := os.Stat(result.BinaryPath); err != nil {
|
|
t.Fatalf("expected replaced plugin binary to exist: %v", err)
|
|
}
|
|
if _, err := os.Stat(result.ManifestPath); err != nil {
|
|
t.Fatalf("expected replaced manifest to exist: %v", err)
|
|
}
|
|
|
|
update := store.updateInputs[0]
|
|
if update.Version == nil || *update.Version != "0.0.19" {
|
|
t.Fatalf("update version = %v, want 0.0.19", update.Version)
|
|
}
|
|
if update.InstallPath == nil || *update.InstallPath != result.BinaryPath {
|
|
t.Fatalf("update install_path = %v, want %q", update.InstallPath, result.BinaryPath)
|
|
}
|
|
if update.Enabled == nil || !*update.Enabled {
|
|
t.Fatalf("update enabled = %v, want true", update.Enabled)
|
|
}
|
|
if len(update.Capabilities) != 1 || update.Capabilities[0].ID != "metadb" {
|
|
t.Fatalf("update capabilities = %#v, want metadata_provider.v1/metadb", update.Capabilities)
|
|
}
|
|
}
|
|
|
|
func assertStoredPluginArchive(t *testing.T, archiveBytes, manifestBytes, binaryData []byte) {
|
|
t.Helper()
|
|
|
|
reader, err := zip.NewReader(bytes.NewReader(archiveBytes), int64(len(archiveBytes)))
|
|
if err != nil {
|
|
t.Fatalf("stored archive is not a zip: %v", err)
|
|
}
|
|
|
|
files := make(map[string]*zip.File, len(reader.File))
|
|
for _, file := range reader.File {
|
|
files[file.Name] = file
|
|
}
|
|
|
|
manifestFile, ok := files["manifest.json"]
|
|
if !ok {
|
|
t.Fatal("stored archive is missing manifest.json")
|
|
}
|
|
gotManifest, err := readZipFile(manifestFile)
|
|
if err != nil {
|
|
t.Fatalf("read manifest.json from stored archive: %v", err)
|
|
}
|
|
if !bytes.Equal(gotManifest, manifestBytes) {
|
|
t.Fatal("stored archive manifest does not match saved manifest bytes")
|
|
}
|
|
|
|
binaryFile, ok := files["plugin"]
|
|
if !ok {
|
|
t.Fatal("stored archive is missing plugin")
|
|
}
|
|
gotBinary, err := readZipFile(binaryFile)
|
|
if err != nil {
|
|
t.Fatalf("read plugin from stored archive: %v", err)
|
|
}
|
|
if !bytes.Equal(gotBinary, binaryData) {
|
|
t.Fatal("stored archive binary does not match installed binary")
|
|
}
|
|
}
|
|
|
|
func testPluginManifest(t *testing.T, pluginID, version string) *pluginv1.PluginManifest {
|
|
t.Helper()
|
|
|
|
manifest := &pluginv1.PluginManifest{
|
|
PluginId: pluginID,
|
|
Version: version,
|
|
Checksum: "sha256-placeholder",
|
|
SiloApiVersion: DefaultSiloAPIVersion,
|
|
SupportedPlatforms: []*pluginv1.SupportedPlatform{
|
|
{Os: "darwin", Arch: "arm64"},
|
|
},
|
|
Capabilities: []*pluginv1.CapabilityDescriptor{
|
|
{
|
|
Type: "metadata_provider.v1",
|
|
Id: "metadb",
|
|
DisplayName: "MetaDB",
|
|
},
|
|
},
|
|
}
|
|
|
|
if _, err := protojson.Marshal(manifest); err != nil {
|
|
t.Fatalf("protojson.Marshal() returned error: %v", err)
|
|
}
|
|
|
|
return manifest
|
|
}
|
|
|
|
type recordingInstallationStore struct {
|
|
createInputs []CreateInstallationInput
|
|
updateIDs []int
|
|
updateInputs []UpdateInstallationInput
|
|
deleteIDs []int
|
|
saveArchiveIDs []int
|
|
saveArchiveErr error
|
|
saveArchiveManifest []byte
|
|
saveArchiveChecksum string
|
|
saveArchiveBytes []byte
|
|
updateErr error
|
|
}
|
|
|
|
func newRecordingInstallationStore() *recordingInstallationStore {
|
|
return &recordingInstallationStore{}
|
|
}
|
|
|
|
func (s *recordingInstallationStore) Create(_ context.Context, input CreateInstallationInput) (*Installation, error) {
|
|
s.createInputs = append(s.createInputs, input)
|
|
return &Installation{
|
|
ID: 1,
|
|
PluginID: input.PluginID,
|
|
Version: input.Version,
|
|
InstallPath: input.InstallPath,
|
|
Enabled: input.Enabled,
|
|
}, nil
|
|
}
|
|
|
|
func (s *recordingInstallationStore) SaveArchive(
|
|
_ context.Context,
|
|
installationID int,
|
|
manifestJSON []byte,
|
|
checksum string,
|
|
archiveBytes []byte,
|
|
) error {
|
|
s.saveArchiveIDs = append(s.saveArchiveIDs, installationID)
|
|
s.saveArchiveManifest = append([]byte(nil), manifestJSON...)
|
|
s.saveArchiveChecksum = checksum
|
|
s.saveArchiveBytes = append([]byte(nil), archiveBytes...)
|
|
return s.saveArchiveErr
|
|
}
|
|
|
|
func (s *recordingInstallationStore) Update(_ context.Context, id int, input UpdateInstallationInput) error {
|
|
s.updateIDs = append(s.updateIDs, id)
|
|
s.updateInputs = append(s.updateInputs, input)
|
|
return s.updateErr
|
|
}
|
|
|
|
func (s *recordingInstallationStore) Delete(_ context.Context, id int) error {
|
|
s.deleteIDs = append(s.deleteIDs, id)
|
|
return nil
|
|
}
|