Some checks failed
Go / build (.exe, 386, windows, windows-386) (push) Has been cancelled
Go / build (.exe, amd64, windows, windows-amd64) (push) Has been cancelled
Go / build (.exe, arm64, windows, windows-arm64) (push) Has been cancelled
Go / build (386, freebsd, freebsd-386) (push) Has been cancelled
Go / build (386, linux, linux-386) (push) Has been cancelled
Go / build (386, netbsd, netbsd-386) (push) Has been cancelled
Go / build (386, openbsd, openbsd-386) (push) Has been cancelled
Go / build (386, plan9, plan9-386) (push) Has been cancelled
Go / build (amd64, darwin, darwin-amd64) (push) Has been cancelled
Go / build (amd64, dragonfly, dragonfly-amd64) (push) Has been cancelled
Go / build (amd64, freebsd, freebsd-amd64) (push) Has been cancelled
Go / build (amd64, illumos, illumos-amd64) (push) Has been cancelled
Go / build (amd64, linux, linux-amd64) (push) Has been cancelled
Go / build (amd64, netbsd, netbsd-amd64) (push) Has been cancelled
Go / build (amd64, openbsd, openbsd-amd64) (push) Has been cancelled
Go / build (amd64, plan9, plan9-amd64) (push) Has been cancelled
Go / build (amd64, solaris, solaris-amd64) (push) Has been cancelled
Go / build (arm, 6, linux, linux-armv6) (push) Has been cancelled
Go / build (arm, 7, linux, linux-armv7) (push) Has been cancelled
Go / build (arm, freebsd, freebsd-arm) (push) Has been cancelled
Go / build (arm, netbsd, netbsd-arm) (push) Has been cancelled
Go / build (arm, openbsd, openbsd-arm) (push) Has been cancelled
Go / build (arm, plan9, plan9-arm) (push) Has been cancelled
Go / build (arm64, darwin, darwin-arm64) (push) Has been cancelled
Go / build (arm64, freebsd, freebsd-arm64) (push) Has been cancelled
Go / build (arm64, linux, linux-arm64) (push) Has been cancelled
Go / build (arm64, netbsd, netbsd-arm64) (push) Has been cancelled
Go / build (arm64, openbsd, openbsd-arm64) (push) Has been cancelled
Go / build (loong64, linux, linux-loong64) (push) Has been cancelled
Go / build (mips, linux, linux-mips) (push) Has been cancelled
Go / build (mips64, linux, linux-mips64) (push) Has been cancelled
Go / build (mips64le, linux, linux-mips64le) (push) Has been cancelled
Go / build (mipsle, linux, linux-mipsle) (push) Has been cancelled
Go / build (ppc64, aix, aix-ppc64) (push) Has been cancelled
Go / build (ppc64, linux, linux-ppc64) (push) Has been cancelled
Go / build (ppc64, openbsd, openbsd-ppc64) (push) Has been cancelled
Go / build (ppc64le, linux, linux-ppc64le) (push) Has been cancelled
Go / build (riscv64, freebsd, freebsd-riscv64) (push) Has been cancelled
Go / build (riscv64, linux, linux-riscv64) (push) Has been cancelled
Go / build (riscv64, openbsd, openbsd-riscv64) (push) Has been cancelled
Go / build (s390x, linux, linux-s390x) (push) Has been cancelled
Docker Image / docker (push) Has been cancelled
Go / merge-artifacts (push) Has been cancelled
1816 lines
64 KiB
Go
1816 lines
64 KiB
Go
// internal/pluginmanager/repository.go 持久化插件制品、插件记录、快照、构建、密钥、评审、公告和操作日志。
|
|
|
|
package pluginmanager
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Repository struct {
|
|
db *sql.DB
|
|
now func() time.Time
|
|
}
|
|
|
|
func NewRepository(db *sql.DB) Repository {
|
|
return Repository{
|
|
db: db,
|
|
now: time.Now,
|
|
}
|
|
}
|
|
|
|
func NewRepositoryWithClock(db *sql.DB, now func() time.Time) Repository {
|
|
repo := NewRepository(db)
|
|
if now != nil {
|
|
repo.now = now
|
|
}
|
|
return repo
|
|
}
|
|
|
|
func (r Repository) SaveArtifact(ctx context.Context, artifact ArtifactRecord) error {
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_artifacts(
|
|
id, plugin_id, version, file_name, file_path, sha256, package_sha256, size_bytes,
|
|
artifact_type, runtime_type, runtime_entry, status, metadata_json,
|
|
capabilities_summary_json, extension_points_json, api_version, go_version, go_os, go_arch,
|
|
uploaded_by, error, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
file_name = excluded.file_name,
|
|
file_path = excluded.file_path,
|
|
package_sha256 = excluded.package_sha256,
|
|
status = excluded.status,
|
|
metadata_json = excluded.metadata_json,
|
|
capabilities_summary_json = excluded.capabilities_summary_json,
|
|
extension_points_json = excluded.extension_points_json,
|
|
uploaded_by = excluded.uploaded_by,
|
|
error = excluded.error,
|
|
updated_at = excluded.updated_at`,
|
|
artifact.ID, artifact.PluginID, artifact.Version, artifact.FileName, artifact.FilePath, artifact.SHA256, artifact.PackageSHA256, artifact.SizeBytes,
|
|
artifact.ArtifactType, artifact.RuntimeType, artifact.RuntimeEntry, artifact.Status, artifact.MetadataJSON,
|
|
artifact.CapabilitiesSummaryJSON, artifact.ExtensionPointsJSON, artifact.APIVersion, artifact.GoVersion, artifact.GOOS, artifact.GOARCH,
|
|
artifact.UploadedBy, artifact.Error, artifact.CreatedAt, artifact.UpdatedAt)
|
|
return err
|
|
}
|
|
|
|
func (r Repository) Artifact(ctx context.Context, id string) (ArtifactRecord, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT id, plugin_id, version, file_name, file_path, sha256, package_sha256, size_bytes,
|
|
artifact_type, runtime_type, runtime_entry, status, metadata_json,
|
|
capabilities_summary_json, extension_points_json, api_version, go_version, go_os, go_arch,
|
|
uploaded_by, error, created_at, updated_at
|
|
FROM plugin_artifacts
|
|
WHERE id = ?`, id)
|
|
return scanArtifact(row)
|
|
}
|
|
|
|
func (r Repository) ListArtifacts(ctx context.Context, pluginID string) ([]ArtifactRecord, error) {
|
|
query := `
|
|
SELECT id, plugin_id, version, file_name, file_path, sha256, package_sha256, size_bytes,
|
|
artifact_type, runtime_type, runtime_entry, status, metadata_json,
|
|
capabilities_summary_json, extension_points_json, api_version, go_version, go_os, go_arch,
|
|
uploaded_by, error, created_at, updated_at
|
|
FROM plugin_artifacts`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ?`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY created_at DESC, id DESC`
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var artifacts []ArtifactRecord
|
|
for rows.Next() {
|
|
artifact, err := scanArtifact(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
artifacts = append(artifacts, artifact)
|
|
}
|
|
return artifacts, rows.Err()
|
|
}
|
|
|
|
func (r Repository) UpsertDesired(ctx context.Context, actor, pluginID, artifactID, desiredState, configJSON string, priority int) (PluginRecord, error) {
|
|
if desiredState == "" {
|
|
desiredState = DesiredDisabled
|
|
}
|
|
if configJSON == "" {
|
|
configJSON = "{}"
|
|
}
|
|
if !json.Valid([]byte(configJSON)) {
|
|
return PluginRecord{}, errors.New("config_json must be valid JSON")
|
|
}
|
|
if priority == 0 {
|
|
priority = DefaultPriority
|
|
}
|
|
switch desiredState {
|
|
case DesiredEnabled, DesiredDisabled, DesiredDeleted:
|
|
default:
|
|
return PluginRecord{}, errors.New("invalid desired_state")
|
|
}
|
|
artifact, err := r.Artifact(ctx, artifactID)
|
|
if err != nil {
|
|
return PluginRecord{}, err
|
|
}
|
|
if artifact.PluginID != pluginID {
|
|
return PluginRecord{}, errors.New("artifact plugin_id does not match")
|
|
}
|
|
if artifact.ArtifactType != ArtifactTypeBinary {
|
|
return PluginRecord{}, errors.New("desired artifact must be a binary artifact")
|
|
}
|
|
|
|
now := r.now().Unix()
|
|
tx, err := r.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return PluginRecord{}, err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
var existing PluginRecord
|
|
row := tx.QueryRowContext(ctx, `
|
|
SELECT id, desired_artifact_id, active_artifact_id, loaded_artifact_id, desired_state, runtime_state,
|
|
priority, config_json, desired_generation, applied_generation, last_error,
|
|
runtime_summary_json, dispatch_summary_json, created_at, updated_at, updated_by
|
|
FROM plugins WHERE id = ?`, pluginID)
|
|
err = scanPluginRow(row, &existing)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return PluginRecord{}, err
|
|
}
|
|
|
|
nextGeneration := int64(1)
|
|
createdAt := now
|
|
if err == nil {
|
|
nextGeneration = existing.DesiredGeneration + 1
|
|
createdAt = existing.CreatedAt
|
|
if _, err := tx.ExecContext(ctx, `
|
|
INSERT INTO plugin_config_snapshots(plugin_id, artifact_id, config_json, desired_state, priority, desired_generation, created_by, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
existing.ID, existing.DesiredArtifactID, existing.ConfigJSON, existing.DesiredState, existing.Priority, existing.DesiredGeneration, actor, now); err != nil {
|
|
return PluginRecord{}, err
|
|
}
|
|
}
|
|
|
|
if _, err := tx.ExecContext(ctx, `
|
|
INSERT INTO plugins(
|
|
id, desired_artifact_id, active_artifact_id, loaded_artifact_id, desired_state, runtime_state,
|
|
priority, config_json, desired_generation, applied_generation, last_error,
|
|
runtime_summary_json, dispatch_summary_json, deleted_at, created_at, updated_at, updated_by
|
|
) VALUES (?, ?, '', '', ?, ?, ?, ?, ?, 0, '', '{}', '{}', 0, ?, ?, ?)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
desired_artifact_id = excluded.desired_artifact_id,
|
|
desired_state = excluded.desired_state,
|
|
priority = excluded.priority,
|
|
config_json = excluded.config_json,
|
|
desired_generation = excluded.desired_generation,
|
|
deleted_at = CASE WHEN excluded.desired_state = 'deleted' THEN excluded.updated_at ELSE 0 END,
|
|
updated_at = excluded.updated_at,
|
|
updated_by = excluded.updated_by`,
|
|
pluginID, artifactID, desiredState, RuntimeDisabled, priority, configJSON, nextGeneration, createdAt, now, actor); err != nil {
|
|
return PluginRecord{}, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return PluginRecord{}, err
|
|
}
|
|
return r.Plugin(ctx, pluginID)
|
|
}
|
|
|
|
func (r Repository) RestoreSnapshot(ctx context.Context, actor string, snapshot ConfigSnapshotRecord) (PluginRecord, error) {
|
|
if snapshot.PluginID == "" {
|
|
return PluginRecord{}, errors.New("snapshot plugin_id is required")
|
|
}
|
|
return r.UpsertDesired(ctx, actor, snapshot.PluginID, snapshot.ArtifactID, snapshot.DesiredState, snapshot.ConfigJSON, snapshot.Priority)
|
|
}
|
|
|
|
func (r Repository) Plugin(ctx context.Context, id string) (PluginRecord, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT id, desired_artifact_id, active_artifact_id, loaded_artifact_id, desired_state, runtime_state,
|
|
priority, config_json, desired_generation, applied_generation, last_error,
|
|
runtime_summary_json, dispatch_summary_json, created_at, updated_at, updated_by
|
|
FROM plugins
|
|
WHERE id = ? AND desired_state <> 'deleted'`, id)
|
|
var plugin PluginRecord
|
|
if err := scanPluginRow(row, &plugin); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return PluginRecord{}, ErrPluginNotFound
|
|
}
|
|
return PluginRecord{}, err
|
|
}
|
|
return plugin, nil
|
|
}
|
|
|
|
func (r Repository) ListPlugins(ctx context.Context) ([]PluginRecord, error) {
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT id, desired_artifact_id, active_artifact_id, loaded_artifact_id, desired_state, runtime_state,
|
|
priority, config_json, desired_generation, applied_generation, last_error,
|
|
runtime_summary_json, dispatch_summary_json, created_at, updated_at, updated_by
|
|
FROM plugins
|
|
WHERE desired_state <> 'deleted'
|
|
ORDER BY priority ASC, id ASC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var plugins []PluginRecord
|
|
for rows.Next() {
|
|
var plugin PluginRecord
|
|
if err := scanPluginRow(rows, &plugin); err != nil {
|
|
return nil, err
|
|
}
|
|
plugins = append(plugins, plugin)
|
|
}
|
|
return plugins, rows.Err()
|
|
}
|
|
|
|
func (r Repository) ListConfigSnapshots(ctx context.Context, pluginID string) ([]ConfigSnapshotRecord, error) {
|
|
query := `
|
|
SELECT id, plugin_id, artifact_id, config_json, desired_state, priority, desired_generation, created_by, created_at
|
|
FROM plugin_config_snapshots`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ?`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY created_at DESC, id DESC`
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var snapshots []ConfigSnapshotRecord
|
|
for rows.Next() {
|
|
snapshot, err := scanConfigSnapshot(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snapshots = append(snapshots, snapshot)
|
|
}
|
|
return snapshots, rows.Err()
|
|
}
|
|
|
|
func (r Repository) ConfigSnapshot(ctx context.Context, id int64) (ConfigSnapshotRecord, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT id, plugin_id, artifact_id, config_json, desired_state, priority, desired_generation, created_by, created_at
|
|
FROM plugin_config_snapshots
|
|
WHERE id = ?`, id)
|
|
return scanConfigSnapshot(row)
|
|
}
|
|
|
|
func (r Repository) DesiredEnabled(ctx context.Context) ([]PluginRecord, error) {
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT id, desired_artifact_id, active_artifact_id, loaded_artifact_id, desired_state, runtime_state,
|
|
priority, config_json, desired_generation, applied_generation, last_error,
|
|
runtime_summary_json, dispatch_summary_json, created_at, updated_at, updated_by
|
|
FROM plugins
|
|
WHERE desired_state = 'enabled'
|
|
ORDER BY priority ASC, id ASC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var plugins []PluginRecord
|
|
for rows.Next() {
|
|
var plugin PluginRecord
|
|
if err := scanPluginRow(rows, &plugin); err != nil {
|
|
return nil, err
|
|
}
|
|
plugins = append(plugins, plugin)
|
|
}
|
|
return plugins, rows.Err()
|
|
}
|
|
|
|
func (r Repository) MarkRuntime(ctx context.Context, pluginID, runtimeState, activeArtifactID, loadedArtifactID string, appliedGeneration int64, lastError string, runtimeSummary, dispatchSummary any) error {
|
|
now := r.now().Unix()
|
|
runtimeJSON, err := marshalDefaultObject(runtimeSummary)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dispatchJSON, err := marshalDefaultObject(dispatchSummary)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = r.db.ExecContext(ctx, `
|
|
UPDATE plugins
|
|
SET runtime_state = ?, active_artifact_id = ?, loaded_artifact_id = ?, applied_generation = ?,
|
|
last_error = ?, runtime_summary_json = ?, dispatch_summary_json = ?, updated_at = ?
|
|
WHERE id = ?`,
|
|
runtimeState, activeArtifactID, loadedArtifactID, appliedGeneration, lastError, runtimeJSON, dispatchJSON, now, pluginID)
|
|
return err
|
|
}
|
|
|
|
func (r Repository) PluginServiceState(ctx context.Context) (PluginServiceState, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT desired_mode, active_mode, applied_at, live_migration, last_error, updated_by, updated_at
|
|
FROM plugin_service_state WHERE id = 1`)
|
|
state, err := scanPluginServiceState(row)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
now := r.now().Unix()
|
|
_, err = r.db.ExecContext(ctx, `
|
|
INSERT OR IGNORE INTO plugin_service_state(id, desired_mode, active_mode, applied_at, live_migration, updated_by, updated_at)
|
|
VALUES (1, ?, ?, ?, ?, ?, ?)`,
|
|
PluginServiceModeInProcess, PluginServiceModeInProcess, now, PluginMigrationDrainOnly, "system", now)
|
|
if err != nil {
|
|
return PluginServiceState{}, err
|
|
}
|
|
return PluginServiceState{
|
|
DesiredMode: PluginServiceModeInProcess,
|
|
ActiveMode: PluginServiceModeInProcess,
|
|
AppliedAt: now,
|
|
LiveMigration: PluginMigrationDrainOnly,
|
|
UpdatedBy: "system",
|
|
UpdatedAt: now,
|
|
}, nil
|
|
}
|
|
state = normalizePluginServiceState(state)
|
|
return state, err
|
|
}
|
|
|
|
func (r Repository) SetPluginServiceDesired(ctx context.Context, actor, mode string) (PluginServiceState, error) {
|
|
now := r.now().Unix()
|
|
if _, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_service_state(id, desired_mode, active_mode, applied_at, live_migration, updated_by, updated_at)
|
|
VALUES (1, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(id) DO UPDATE SET desired_mode = excluded.desired_mode, updated_by = excluded.updated_by, updated_at = excluded.updated_at`,
|
|
mode, PluginServiceModeInProcess, 0, PluginMigrationDrainOnly, actor, now); err != nil {
|
|
return PluginServiceState{}, err
|
|
}
|
|
return r.PluginServiceState(ctx)
|
|
}
|
|
|
|
func (r Repository) ApplyPluginServiceActive(ctx context.Context, mode string) (PluginServiceState, error) {
|
|
now := r.now().Unix()
|
|
if _, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_service_state(id, desired_mode, active_mode, applied_at, live_migration, last_error, updated_by, updated_at)
|
|
VALUES (1, ?, ?, ?, ?, '', 'system', ?)
|
|
ON CONFLICT(id) DO UPDATE SET active_mode = excluded.active_mode, applied_at = excluded.applied_at, last_error = '', updated_at = excluded.updated_at`,
|
|
mode, mode, now, PluginMigrationDrainOnly, now); err != nil {
|
|
return PluginServiceState{}, err
|
|
}
|
|
return r.PluginServiceState(ctx)
|
|
}
|
|
|
|
func (r Repository) SetPluginServiceError(ctx context.Context, message string) error {
|
|
_, err := r.db.ExecContext(ctx, `UPDATE plugin_service_state SET last_error = ?, updated_at = ? WHERE id = 1`, message, r.now().Unix())
|
|
return err
|
|
}
|
|
|
|
func (r Repository) UpdateArtifactStatus(ctx context.Context, artifactID, status, message string) error {
|
|
_, err := r.db.ExecContext(ctx, `UPDATE plugin_artifacts SET status = ?, error = ?, updated_at = ? WHERE id = ?`,
|
|
status, message, r.now().Unix(), artifactID)
|
|
return err
|
|
}
|
|
|
|
func (r Repository) CreateBuild(ctx context.Context, build BuildRecord) (BuildRecord, error) {
|
|
now := r.now().Unix()
|
|
build.CreatedAt = now
|
|
build.UpdatedAt = now
|
|
if build.Status == "" {
|
|
build.Status = BuildStatusQueued
|
|
}
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_builds(
|
|
plugin_id, source_id, artifact_id, status, builder_type, builder_image, builder_version,
|
|
go_version, go_os, go_arch, go_amd64, go_arm64, cgo_enabled, build_tags,
|
|
sdk_module, sdk_version, go_proxy, go_no_sumdb, go_private, vendor_required,
|
|
source_sha256, artifact_sha256, module_summary_json, go_version_m_json, abi_fingerprint,
|
|
log_summary, metadata_json, error, started_at, ended_at, duration_ms, created_by, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
build.PluginID, build.SourceID, build.ArtifactID, build.Status, build.BuilderType, build.BuilderImage, build.BuilderVersion,
|
|
build.GoVersion, build.GOOS, build.GOARCH, build.GOAMD64, build.GOARM64, build.CGOEnabled, build.BuildTags,
|
|
build.SDKModule, build.SDKVersion, build.GOPROXY, build.GONOSUMDB, build.GOPRIVATE, boolInt(build.VendorRequired),
|
|
build.SourceSHA256, build.ArtifactSHA256, defaultJSONArray(build.ModuleSummary), defaultJSONObject(build.GoVersionM), build.ABIFingerprint,
|
|
build.LogSummary, defaultJSONObject(build.MetadataJSON), build.Error, build.StartedAt, build.EndedAt, build.DurationMS, build.CreatedBy, build.CreatedAt, build.UpdatedAt)
|
|
if err != nil {
|
|
return BuildRecord{}, err
|
|
}
|
|
id, err := lastInsertID(ctx, r.db)
|
|
if err != nil {
|
|
return BuildRecord{}, err
|
|
}
|
|
return r.Build(ctx, id)
|
|
}
|
|
|
|
func (r Repository) Build(ctx context.Context, id int64) (BuildRecord, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT id, plugin_id, source_id, artifact_id, status, builder_type, builder_image, builder_version,
|
|
go_version, go_os, go_arch, go_amd64, go_arm64, cgo_enabled, build_tags,
|
|
sdk_module, sdk_version, go_proxy, go_no_sumdb, go_private, vendor_required,
|
|
source_sha256, artifact_sha256, module_summary_json, go_version_m_json, abi_fingerprint,
|
|
log_summary, metadata_json, error, started_at, ended_at, duration_ms, created_by, created_at, updated_at
|
|
FROM plugin_builds WHERE id = ?`, id)
|
|
return scanBuild(row)
|
|
}
|
|
|
|
func (r Repository) ListBuilds(ctx context.Context, pluginID string) ([]BuildRecord, error) {
|
|
query := `
|
|
SELECT id, plugin_id, source_id, artifact_id, status, builder_type, builder_image, builder_version,
|
|
go_version, go_os, go_arch, go_amd64, go_arm64, cgo_enabled, build_tags,
|
|
sdk_module, sdk_version, go_proxy, go_no_sumdb, go_private, vendor_required,
|
|
source_sha256, artifact_sha256, module_summary_json, go_version_m_json, abi_fingerprint,
|
|
log_summary, metadata_json, error, started_at, ended_at, duration_ms, created_by, created_at, updated_at
|
|
FROM plugin_builds`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ?`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY created_at DESC, id DESC`
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var builds []BuildRecord
|
|
for rows.Next() {
|
|
build, err := scanBuild(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
builds = append(builds, build)
|
|
}
|
|
return builds, rows.Err()
|
|
}
|
|
|
|
func (r Repository) MarkBuildRunning(ctx context.Context, id int64) error {
|
|
now := r.now().Unix()
|
|
_, err := r.db.ExecContext(ctx, `UPDATE plugin_builds SET status = ?, started_at = ?, updated_at = ? WHERE id = ? AND status = ?`,
|
|
BuildStatusRunning, now, now, id, BuildStatusQueued)
|
|
return err
|
|
}
|
|
|
|
func (r Repository) FinishBuild(ctx context.Context, build BuildRecord) error {
|
|
now := r.now().Unix()
|
|
if build.EndedAt == 0 {
|
|
build.EndedAt = now
|
|
}
|
|
if build.StartedAt > 0 && build.DurationMS == 0 {
|
|
build.DurationMS = (build.EndedAt - build.StartedAt) * 1000
|
|
}
|
|
_, err := r.db.ExecContext(ctx, `
|
|
UPDATE plugin_builds SET
|
|
artifact_id = ?, status = ?, builder_image = ?, builder_version = ?, go_version = ?,
|
|
go_os = ?, go_arch = ?, go_amd64 = ?, go_arm64 = ?, cgo_enabled = ?, build_tags = ?,
|
|
sdk_module = ?, sdk_version = ?, go_proxy = ?, go_no_sumdb = ?, go_private = ?, vendor_required = ?,
|
|
source_sha256 = ?, artifact_sha256 = ?, module_summary_json = ?, go_version_m_json = ?,
|
|
abi_fingerprint = ?, log_summary = ?, metadata_json = ?, error = ?, ended_at = ?, duration_ms = ?, updated_at = ?
|
|
WHERE id = ?`,
|
|
build.ArtifactID, build.Status, build.BuilderImage, build.BuilderVersion, build.GoVersion,
|
|
build.GOOS, build.GOARCH, build.GOAMD64, build.GOARM64, build.CGOEnabled, build.BuildTags,
|
|
build.SDKModule, build.SDKVersion, build.GOPROXY, build.GONOSUMDB, build.GOPRIVATE, boolInt(build.VendorRequired),
|
|
build.SourceSHA256, build.ArtifactSHA256, defaultJSONArray(build.ModuleSummary), defaultJSONObject(build.GoVersionM),
|
|
build.ABIFingerprint, build.LogSummary, defaultJSONObject(build.MetadataJSON), build.Error, build.EndedAt, build.DurationMS, now,
|
|
build.ID)
|
|
return err
|
|
}
|
|
|
|
func (r Repository) CancelBuild(ctx context.Context, id int64, actor string) (BuildRecord, error) {
|
|
now := r.now().Unix()
|
|
res, err := r.db.ExecContext(ctx, `
|
|
UPDATE plugin_builds
|
|
SET status = ?, error = ?, ended_at = ?, updated_at = ?
|
|
WHERE id = ? AND status IN (?, ?)`,
|
|
BuildStatusCanceled, "build canceled by "+actor, now, now, id, BuildStatusQueued, BuildStatusRunning)
|
|
if err != nil {
|
|
return BuildRecord{}, err
|
|
}
|
|
if rows, _ := res.RowsAffected(); rows == 0 {
|
|
return r.Build(ctx, id)
|
|
}
|
|
return r.Build(ctx, id)
|
|
}
|
|
|
|
func (r Repository) RetryBuild(ctx context.Context, id int64, actor string) (BuildRecord, error) {
|
|
previous, err := r.Build(ctx, id)
|
|
if err != nil {
|
|
return BuildRecord{}, err
|
|
}
|
|
if previous.Status != BuildStatusFailed && previous.Status != BuildStatusCanceled {
|
|
return BuildRecord{}, fmt.Errorf("build status %q cannot be retried", previous.Status)
|
|
}
|
|
previous.ID = 0
|
|
previous.ArtifactID = ""
|
|
previous.ArtifactSHA256 = ""
|
|
previous.Status = BuildStatusQueued
|
|
previous.LogSummary = ""
|
|
previous.Error = ""
|
|
previous.StartedAt = 0
|
|
previous.EndedAt = 0
|
|
previous.DurationMS = 0
|
|
previous.CreatedBy = actor
|
|
return r.CreateBuild(ctx, previous)
|
|
}
|
|
|
|
func (r Repository) ReferencedArtifactIDs(ctx context.Context) (map[string]bool, error) {
|
|
refs := make(map[string]bool)
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT desired_artifact_id, active_artifact_id, loaded_artifact_id
|
|
FROM plugins
|
|
WHERE desired_state <> 'deleted'`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var desired, active, loaded string
|
|
if err := rows.Scan(&desired, &active, &loaded); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
for _, id := range []string{desired, active, loaded} {
|
|
if id != "" {
|
|
refs[id] = true
|
|
}
|
|
}
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err = r.db.QueryContext(ctx, `SELECT artifact_id FROM plugin_config_snapshots WHERE artifact_id <> ''`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var id string
|
|
if err := rows.Scan(&id); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
if id != "" {
|
|
refs[id] = true
|
|
}
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
rows, err = r.db.QueryContext(ctx, `SELECT artifact_id FROM plugin_builds WHERE artifact_id <> '' AND status = ?`, BuildStatusSucceeded)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for rows.Next() {
|
|
var id string
|
|
if err := rows.Scan(&id); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
if id != "" {
|
|
refs[id] = true
|
|
}
|
|
}
|
|
if err := rows.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
return refs, nil
|
|
}
|
|
|
|
func (r Repository) UpsertSecret(ctx context.Context, actor, pluginID, name, value string, reloadRequired, hotReload bool) (SecretRecord, error) {
|
|
if pluginID == "" {
|
|
return SecretRecord{}, errors.New("plugin_id is required")
|
|
}
|
|
if !pluginIDPattern.MatchString(pluginID) {
|
|
return SecretRecord{}, fmt.Errorf("invalid plugin id %q", pluginID)
|
|
}
|
|
if !secretNamePattern.MatchString(name) {
|
|
return SecretRecord{}, fmt.Errorf("invalid secret name %q", name)
|
|
}
|
|
if value == "" {
|
|
return SecretRecord{}, errors.New("secret value is required")
|
|
}
|
|
now := r.now().Unix()
|
|
tx, err := r.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return SecretRecord{}, err
|
|
}
|
|
defer tx.Rollback()
|
|
var currentVersion int64
|
|
var currentValue string
|
|
err = tx.QueryRowContext(ctx, `SELECT current_version, current_value FROM plugin_secrets WHERE plugin_id = ? AND name = ?`, pluginID, name).Scan(¤tVersion, ¤tValue)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return SecretRecord{}, err
|
|
}
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
_, err = tx.ExecContext(ctx, `
|
|
INSERT INTO plugin_secrets(plugin_id, name, current_version, previous_version, current_value, previous_value, reload_required, hot_reload, updated_by, created_at, updated_at)
|
|
VALUES (?, ?, 1, 0, ?, '', ?, ?, ?, ?, ?)`,
|
|
pluginID, name, value, boolInt(reloadRequired), boolInt(hotReload), actor, now, now)
|
|
} else {
|
|
_, err = tx.ExecContext(ctx, `
|
|
UPDATE plugin_secrets
|
|
SET previous_version = current_version,
|
|
previous_value = current_value,
|
|
current_version = current_version + 1,
|
|
current_value = ?,
|
|
reload_required = ?,
|
|
hot_reload = ?,
|
|
updated_by = ?,
|
|
updated_at = ?
|
|
WHERE plugin_id = ? AND name = ?`,
|
|
value, boolInt(reloadRequired), boolInt(hotReload), actor, now, pluginID, name)
|
|
}
|
|
if err != nil {
|
|
return SecretRecord{}, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return SecretRecord{}, err
|
|
}
|
|
return r.Secret(ctx, pluginID, name)
|
|
}
|
|
|
|
func (r Repository) Secret(ctx context.Context, pluginID, name string) (SecretRecord, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT plugin_id, name, current_version, previous_version, reload_required, hot_reload, updated_by, created_at, updated_at
|
|
FROM plugin_secrets
|
|
WHERE plugin_id = ? AND name = ?`, pluginID, name)
|
|
return scanSecret(row)
|
|
}
|
|
|
|
func (r Repository) ListSecrets(ctx context.Context, pluginID string) ([]SecretRecord, error) {
|
|
query := `
|
|
SELECT plugin_id, name, current_version, previous_version, reload_required, hot_reload, updated_by, created_at, updated_at
|
|
FROM plugin_secrets`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ?`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY plugin_id ASC, name ASC`
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var secrets []SecretRecord
|
|
for rows.Next() {
|
|
secret, err := scanSecret(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
secrets = append(secrets, secret)
|
|
}
|
|
return secrets, rows.Err()
|
|
}
|
|
|
|
func (r Repository) SaveReview(ctx context.Context, review ReviewRecord) (ReviewRecord, error) {
|
|
now := r.now().Unix()
|
|
if review.CreatedAt == 0 {
|
|
review.CreatedAt = now
|
|
}
|
|
if review.Profile == "" {
|
|
review.Profile = PolicyProfileDev
|
|
}
|
|
if review.RiskLevel == "" {
|
|
review.RiskLevel = RiskLow
|
|
}
|
|
if review.Decision == "" {
|
|
review.Decision = ReviewDecisionApproved
|
|
}
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_reviews(
|
|
plugin_id, artifact_id, profile, risk_level, config_hash, scope_hash, rollout_hash,
|
|
runtime_limits_hash, features_hash, policy_hash, decision, notes, reviewed_by, created_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
review.PluginID, review.ArtifactID, review.Profile, review.RiskLevel, review.ConfigHash, review.ScopeHash, review.RolloutHash,
|
|
review.RuntimeLimitsHash, review.FeaturesHash, review.PolicyHash, review.Decision, review.Notes, review.ReviewedBy, review.CreatedAt)
|
|
if err != nil {
|
|
return ReviewRecord{}, err
|
|
}
|
|
id, err := lastInsertID(ctx, r.db)
|
|
if err != nil {
|
|
return ReviewRecord{}, err
|
|
}
|
|
review.ID = id
|
|
return review, nil
|
|
}
|
|
|
|
func (r Repository) ListReviews(ctx context.Context, pluginID string) ([]ReviewRecord, error) {
|
|
query := `
|
|
SELECT id, plugin_id, artifact_id, profile, risk_level, config_hash, scope_hash, rollout_hash,
|
|
runtime_limits_hash, features_hash, policy_hash, decision, notes, reviewed_by, created_at
|
|
FROM plugin_reviews`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ?`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY created_at DESC, id DESC`
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var reviews []ReviewRecord
|
|
for rows.Next() {
|
|
review, err := scanReview(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reviews = append(reviews, review)
|
|
}
|
|
return reviews, rows.Err()
|
|
}
|
|
|
|
func (r Repository) ApprovedReview(ctx context.Context, pluginID, artifactID, profile, policyHash, configHash, scopeHash, rolloutHash, runtimeHash, featuresHash string) (ReviewRecord, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT id, plugin_id, artifact_id, profile, risk_level, config_hash, scope_hash, rollout_hash,
|
|
runtime_limits_hash, features_hash, policy_hash, decision, notes, reviewed_by, created_at
|
|
FROM plugin_reviews
|
|
WHERE plugin_id = ? AND artifact_id = ? AND profile = ? AND policy_hash = ?
|
|
AND config_hash = ? AND scope_hash = ? AND rollout_hash = ? AND runtime_limits_hash = ? AND features_hash = ?
|
|
AND decision = ?
|
|
ORDER BY created_at DESC, id DESC
|
|
LIMIT 1`, pluginID, artifactID, profile, policyHash, configHash, scopeHash, rolloutHash, runtimeHash, featuresHash, ReviewDecisionApproved)
|
|
review, err := scanReview(row)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return ReviewRecord{}, nil
|
|
}
|
|
return review, err
|
|
}
|
|
|
|
func (r Repository) SaveWarningOverride(ctx context.Context, override WarningOverrideRecord) (WarningOverrideRecord, error) {
|
|
now := r.now().Unix()
|
|
if override.CreatedAt == 0 {
|
|
override.CreatedAt = now
|
|
}
|
|
if override.Profile == "" {
|
|
override.Profile = PolicyProfileDev
|
|
}
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_warning_overrides(plugin_id, artifact_id, profile, action, policy_hash, reason, created_by, expires_at, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
override.PluginID, override.ArtifactID, override.Profile, override.Action, override.PolicyHash, override.Reason, override.CreatedBy, override.ExpiresAt, override.CreatedAt)
|
|
if err != nil {
|
|
return WarningOverrideRecord{}, err
|
|
}
|
|
id, err := lastInsertID(ctx, r.db)
|
|
if err != nil {
|
|
return WarningOverrideRecord{}, err
|
|
}
|
|
override.ID = id
|
|
return override, nil
|
|
}
|
|
|
|
func (r Repository) ActiveWarningOverride(ctx context.Context, pluginID, artifactID, profile, action, policyHash string) (WarningOverrideRecord, bool, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT id, plugin_id, artifact_id, profile, action, policy_hash, reason, created_by, expires_at, created_at
|
|
FROM plugin_warning_overrides
|
|
WHERE plugin_id = ? AND artifact_id = ? AND profile = ? AND action = ? AND policy_hash = ? AND expires_at > ?
|
|
ORDER BY expires_at DESC, id DESC
|
|
LIMIT 1`, pluginID, artifactID, profile, action, policyHash, r.now().Unix())
|
|
override, err := scanWarningOverride(row)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return WarningOverrideRecord{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return WarningOverrideRecord{}, false, err
|
|
}
|
|
return override, true, nil
|
|
}
|
|
|
|
func (r Repository) ListWarningOverrides(ctx context.Context, pluginID string) ([]WarningOverrideRecord, error) {
|
|
query := `
|
|
SELECT id, plugin_id, artifact_id, profile, action, policy_hash, reason, created_by, expires_at, created_at
|
|
FROM plugin_warning_overrides`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ?`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY created_at DESC, id DESC`
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var overrides []WarningOverrideRecord
|
|
for rows.Next() {
|
|
override, err := scanWarningOverride(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
overrides = append(overrides, override)
|
|
}
|
|
return overrides, rows.Err()
|
|
}
|
|
|
|
func (r Repository) UpsertAdvisory(ctx context.Context, actor string, req AdvisoryRequest) (AdvisoryRecord, error) {
|
|
now := r.now().Unix()
|
|
if req.Status == "" {
|
|
req.Status = AdvisoryStatusActive
|
|
}
|
|
if req.Action == "" {
|
|
req.Action = AdvisoryActionDenylist
|
|
}
|
|
if strings.TrimSpace(req.AdvisoryID) == "" {
|
|
return AdvisoryRecord{}, errors.New("advisory_id is required")
|
|
}
|
|
res, err := r.db.ExecContext(ctx, `
|
|
UPDATE plugin_advisories
|
|
SET status = ?, action = ?, artifact_sha256 = ?, plugin_id = ?, version_range = ?,
|
|
dependency_name = ?, dependency_range = ?, recommended_action = ?, fixed_version = ?,
|
|
mitigation = ?, created_by = ?, updated_at = ?
|
|
WHERE advisory_id = ?`,
|
|
req.Status, req.Action, req.ArtifactSHA256, req.PluginID, req.VersionRange,
|
|
req.DependencyName, req.DependencyRange, req.RecommendedAction, req.FixedVersion,
|
|
req.Mitigation, actor, now, req.AdvisoryID)
|
|
if err != nil {
|
|
return AdvisoryRecord{}, err
|
|
}
|
|
if rows, _ := res.RowsAffected(); rows == 0 {
|
|
_, err = r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_advisories(
|
|
advisory_id, status, action, artifact_sha256, plugin_id, version_range, dependency_name,
|
|
dependency_range, recommended_action, fixed_version, mitigation, created_by, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
req.AdvisoryID, req.Status, req.Action, req.ArtifactSHA256, req.PluginID, req.VersionRange, req.DependencyName,
|
|
req.DependencyRange, req.RecommendedAction, req.FixedVersion, req.Mitigation, actor, now, now)
|
|
if err != nil {
|
|
return AdvisoryRecord{}, err
|
|
}
|
|
}
|
|
return r.AdvisoryByID(ctx, req.AdvisoryID)
|
|
}
|
|
|
|
func (r Repository) AdvisoryByID(ctx context.Context, advisoryID string) (AdvisoryRecord, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT id, advisory_id, status, action, artifact_sha256, plugin_id, version_range, dependency_name,
|
|
dependency_range, recommended_action, fixed_version, mitigation, created_by, created_at, updated_at
|
|
FROM plugin_advisories
|
|
WHERE advisory_id = ?
|
|
ORDER BY id DESC
|
|
LIMIT 1`, advisoryID)
|
|
return scanAdvisory(row)
|
|
}
|
|
|
|
func (r Repository) ListAdvisories(ctx context.Context, pluginID string) ([]AdvisoryRecord, error) {
|
|
query := `
|
|
SELECT id, advisory_id, status, action, artifact_sha256, plugin_id, version_range, dependency_name,
|
|
dependency_range, recommended_action, fixed_version, mitigation, created_by, created_at, updated_at
|
|
FROM plugin_advisories`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ? OR plugin_id = ''`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY updated_at DESC, id DESC`
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var advisories []AdvisoryRecord
|
|
for rows.Next() {
|
|
advisory, err := scanAdvisory(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
advisories = append(advisories, advisory)
|
|
}
|
|
return advisories, rows.Err()
|
|
}
|
|
|
|
func (r Repository) SaveRepositoryImport(ctx context.Context, record RepositoryImportRecord) (RepositoryImportRecord, error) {
|
|
now := r.now().Unix()
|
|
if record.CreatedAt == 0 {
|
|
record.CreatedAt = now
|
|
}
|
|
if record.AdmissionJSON == "" || !json.Valid([]byte(record.AdmissionJSON)) {
|
|
record.AdmissionJSON = "{}"
|
|
}
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_repository_imports(
|
|
repository_type, index_path, repository_name, candidate_id, plugin_id, version,
|
|
artifact_id, package_sha256, trust_policy, admission_json, imported_by, created_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
record.RepositoryType, record.IndexPath, record.RepositoryName, record.CandidateID, record.PluginID, record.Version,
|
|
record.ArtifactID, record.PackageSHA256, record.TrustPolicy, record.AdmissionJSON, record.ImportedBy, record.CreatedAt)
|
|
if err != nil {
|
|
return RepositoryImportRecord{}, err
|
|
}
|
|
id, err := lastInsertID(ctx, r.db)
|
|
if err != nil {
|
|
return RepositoryImportRecord{}, err
|
|
}
|
|
return r.RepositoryImport(ctx, id)
|
|
}
|
|
|
|
func (r Repository) RepositoryImport(ctx context.Context, id int64) (RepositoryImportRecord, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT id, repository_type, index_path, repository_name, candidate_id, plugin_id, version,
|
|
artifact_id, package_sha256, trust_policy, admission_json, imported_by, created_at
|
|
FROM plugin_repository_imports WHERE id = ?`, id)
|
|
return scanRepositoryImport(row)
|
|
}
|
|
|
|
func (r Repository) ListRepositoryImports(ctx context.Context) ([]RepositoryImportRecord, error) {
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT id, repository_type, index_path, repository_name, candidate_id, plugin_id, version,
|
|
artifact_id, package_sha256, trust_policy, admission_json, imported_by, created_at
|
|
FROM plugin_repository_imports ORDER BY created_at DESC, id DESC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var imports []RepositoryImportRecord
|
|
for rows.Next() {
|
|
record, err := scanRepositoryImport(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
imports = append(imports, record)
|
|
}
|
|
return imports, rows.Err()
|
|
}
|
|
|
|
func (r Repository) SaveSupplyChainAssessment(ctx context.Context, assessment SupplyChainAssessment) (SupplyChainAssessment, error) {
|
|
now := r.now().Unix()
|
|
if assessment.CreatedAt == 0 {
|
|
assessment.CreatedAt = now
|
|
}
|
|
issues, err := json.Marshal(assessment.Issues)
|
|
if err != nil {
|
|
return SupplyChainAssessment{}, err
|
|
}
|
|
signature, err := marshalDefaultObject(assessment.Signature)
|
|
if err != nil {
|
|
return SupplyChainAssessment{}, err
|
|
}
|
|
sbom, err := marshalDefaultObject(assessment.SBOM)
|
|
if err != nil {
|
|
return SupplyChainAssessment{}, err
|
|
}
|
|
license, err := marshalDefaultObject(assessment.License)
|
|
if err != nil {
|
|
return SupplyChainAssessment{}, err
|
|
}
|
|
advisory, err := marshalDefaultObject(assessment.Advisory)
|
|
if err != nil {
|
|
return SupplyChainAssessment{}, err
|
|
}
|
|
metadata, err := marshalDefaultObject(assessment.Metadata)
|
|
if err != nil {
|
|
return SupplyChainAssessment{}, err
|
|
}
|
|
if assessment.Status == "" {
|
|
assessment.Status = SupplyChainStatusAllowed
|
|
}
|
|
_, err = r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_supply_chain_assessments(
|
|
plugin_id, artifact_id, status, issues_json, signature_json, sbom_json,
|
|
license_json, advisory_json, metadata_json, created_by, created_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
assessment.PluginID, assessment.ArtifactID, assessment.Status, string(issues), signature, sbom,
|
|
license, advisory, metadata, assessment.CreatedBy, assessment.CreatedAt)
|
|
if err != nil {
|
|
return SupplyChainAssessment{}, err
|
|
}
|
|
id, err := lastInsertID(ctx, r.db)
|
|
if err != nil {
|
|
return SupplyChainAssessment{}, err
|
|
}
|
|
return r.SupplyChainAssessment(ctx, id)
|
|
}
|
|
|
|
func (r Repository) SupplyChainAssessment(ctx context.Context, id int64) (SupplyChainAssessment, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT id, plugin_id, artifact_id, status, issues_json, signature_json, sbom_json,
|
|
license_json, advisory_json, metadata_json, created_by, created_at
|
|
FROM plugin_supply_chain_assessments WHERE id = ?`, id)
|
|
return scanSupplyChainAssessment(row)
|
|
}
|
|
|
|
func (r Repository) ListSupplyChainAssessments(ctx context.Context, pluginID, artifactID string) ([]SupplyChainAssessment, error) {
|
|
query := `
|
|
SELECT id, plugin_id, artifact_id, status, issues_json, signature_json, sbom_json,
|
|
license_json, advisory_json, metadata_json, created_by, created_at
|
|
FROM plugin_supply_chain_assessments`
|
|
var args []any
|
|
var clauses []string
|
|
if pluginID != "" {
|
|
clauses = append(clauses, "plugin_id = ?")
|
|
args = append(args, pluginID)
|
|
}
|
|
if artifactID != "" {
|
|
clauses = append(clauses, "artifact_id = ?")
|
|
args = append(args, artifactID)
|
|
}
|
|
if len(clauses) > 0 {
|
|
query += " WHERE " + strings.Join(clauses, " AND ")
|
|
}
|
|
query += ` ORDER BY created_at DESC, id DESC`
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var assessments []SupplyChainAssessment
|
|
for rows.Next() {
|
|
assessment, err := scanSupplyChainAssessment(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
assessments = append(assessments, assessment)
|
|
}
|
|
return assessments, rows.Err()
|
|
}
|
|
|
|
func (r Repository) SavePreflight(ctx context.Context, record PreflightRecord) (PreflightRecord, error) {
|
|
now := r.now().Unix()
|
|
if record.CreatedAt == 0 {
|
|
record.CreatedAt = now
|
|
}
|
|
if record.Profile == "" {
|
|
record.Profile = PolicyProfileDev
|
|
}
|
|
if record.ResultJSON == "" || !json.Valid([]byte(record.ResultJSON)) {
|
|
record.ResultJSON = "{}"
|
|
}
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_preflight_results(plugin_id, artifact_id, profile, status, result_json, created_by, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
record.PluginID, record.ArtifactID, record.Profile, record.Status, record.ResultJSON, record.CreatedBy, record.CreatedAt)
|
|
if err != nil {
|
|
return PreflightRecord{}, err
|
|
}
|
|
id, err := lastInsertID(ctx, r.db)
|
|
if err != nil {
|
|
return PreflightRecord{}, err
|
|
}
|
|
record.ID = id
|
|
return record, nil
|
|
}
|
|
|
|
func (r Repository) ListPreflights(ctx context.Context, pluginID string) ([]PreflightRecord, error) {
|
|
query := `
|
|
SELECT id, plugin_id, artifact_id, profile, status, result_json, created_by, created_at
|
|
FROM plugin_preflight_results`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ?`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY created_at DESC, id DESC`
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var records []PreflightRecord
|
|
for rows.Next() {
|
|
record, err := scanPreflight(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
records = append(records, record)
|
|
}
|
|
return records, rows.Err()
|
|
}
|
|
|
|
func (r Repository) SaveBenchmark(ctx context.Context, actor string, req BenchmarkRequest) (BenchmarkRecord, error) {
|
|
now := r.now().Unix()
|
|
if req.Profile == "" {
|
|
req.Profile = PolicyProfileDev
|
|
}
|
|
record := BenchmarkRecord{
|
|
PluginID: "",
|
|
ArtifactID: req.ArtifactID,
|
|
Profile: req.Profile,
|
|
BenchmarkProfile: req.BenchmarkProfile,
|
|
P95MS: req.P95MS,
|
|
P99MS: req.P99MS,
|
|
ErrorRate: req.ErrorRate,
|
|
ActiveProxyCapacity: req.ActiveProxyCapacity,
|
|
BaselineDiff: req.BaselineDiff,
|
|
CreatedBy: actor,
|
|
CreatedAt: now,
|
|
}
|
|
artifact, err := r.Artifact(ctx, req.ArtifactID)
|
|
if err != nil {
|
|
return BenchmarkRecord{}, err
|
|
}
|
|
record.PluginID = artifact.PluginID
|
|
_, err = r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_benchmarks(
|
|
plugin_id, artifact_id, profile, benchmark_profile, p95_ms, p99_ms, error_rate,
|
|
active_proxy_capacity, baseline_diff, created_by, created_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
record.PluginID, record.ArtifactID, record.Profile, record.BenchmarkProfile, record.P95MS, record.P99MS, record.ErrorRate,
|
|
record.ActiveProxyCapacity, record.BaselineDiff, record.CreatedBy, record.CreatedAt)
|
|
if err != nil {
|
|
return BenchmarkRecord{}, err
|
|
}
|
|
id, err := lastInsertID(ctx, r.db)
|
|
if err != nil {
|
|
return BenchmarkRecord{}, err
|
|
}
|
|
record.ID = id
|
|
return record, nil
|
|
}
|
|
|
|
func (r Repository) ListBenchmarks(ctx context.Context, pluginID string) ([]BenchmarkRecord, error) {
|
|
query := `
|
|
SELECT id, plugin_id, artifact_id, profile, benchmark_profile, p95_ms, p99_ms, error_rate,
|
|
active_proxy_capacity, baseline_diff, created_by, created_at
|
|
FROM plugin_benchmarks`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ?`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY created_at DESC, id DESC`
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var records []BenchmarkRecord
|
|
for rows.Next() {
|
|
record, err := scanBenchmark(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
records = append(records, record)
|
|
}
|
|
return records, rows.Err()
|
|
}
|
|
|
|
func (r Repository) LatestBenchmark(ctx context.Context, pluginID, artifactID, profile string) (BenchmarkRecord, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT id, plugin_id, artifact_id, profile, benchmark_profile, p95_ms, p99_ms, error_rate,
|
|
active_proxy_capacity, baseline_diff, created_by, created_at
|
|
FROM plugin_benchmarks
|
|
WHERE plugin_id = ? AND artifact_id = ? AND profile = ?
|
|
ORDER BY created_at DESC, id DESC
|
|
LIMIT 1`, pluginID, artifactID, profile)
|
|
benchmark, err := scanBenchmark(row)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return BenchmarkRecord{}, nil
|
|
}
|
|
return benchmark, err
|
|
}
|
|
|
|
func (r Repository) SaveInstrumentation(ctx context.Context, actor string, req InstrumentationRequest) (InstrumentationRecord, error) {
|
|
now := r.now().Unix()
|
|
if req.Status == "" {
|
|
req.Status = InstrumentationStatusAvailable
|
|
}
|
|
provenance, err := marshalDefaultObject(req.Provenance)
|
|
if err != nil {
|
|
return InstrumentationRecord{}, err
|
|
}
|
|
conformance, err := marshalDefaultObject(req.Conformance)
|
|
if err != nil {
|
|
return InstrumentationRecord{}, err
|
|
}
|
|
benchmark, err := marshalDefaultObject(req.Benchmark)
|
|
if err != nil {
|
|
return InstrumentationRecord{}, err
|
|
}
|
|
smoke, err := marshalDefaultObject(req.Smoke)
|
|
if err != nil {
|
|
return InstrumentationRecord{}, err
|
|
}
|
|
_, err = r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_instrumentation(
|
|
name, version, profile, generated_diff_hash, provenance_json, conformance_json,
|
|
benchmark_json, smoke_json, runbook_rollback, status, created_by, created_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
req.Name, req.Version, req.Profile, req.GeneratedDiffHash, provenance, conformance,
|
|
benchmark, smoke, req.RunbookRollback, req.Status, actor, now)
|
|
if err != nil {
|
|
return InstrumentationRecord{}, err
|
|
}
|
|
id, err := lastInsertID(ctx, r.db)
|
|
if err != nil {
|
|
return InstrumentationRecord{}, err
|
|
}
|
|
return r.Instrumentation(ctx, id)
|
|
}
|
|
|
|
func (r Repository) Instrumentation(ctx context.Context, id int64) (InstrumentationRecord, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT id, name, version, profile, generated_diff_hash, provenance_json, conformance_json,
|
|
benchmark_json, smoke_json, runbook_rollback, status, created_by, created_at
|
|
FROM plugin_instrumentation WHERE id = ?`, id)
|
|
return scanInstrumentation(row)
|
|
}
|
|
|
|
func (r Repository) ListInstrumentation(ctx context.Context) ([]InstrumentationRecord, error) {
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT id, name, version, profile, generated_diff_hash, provenance_json, conformance_json,
|
|
benchmark_json, smoke_json, runbook_rollback, status, created_by, created_at
|
|
FROM plugin_instrumentation ORDER BY created_at DESC, id DESC`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var records []InstrumentationRecord
|
|
for rows.Next() {
|
|
record, err := scanInstrumentation(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
records = append(records, record)
|
|
}
|
|
return records, rows.Err()
|
|
}
|
|
|
|
func (r Repository) RecordOperation(ctx context.Context, pluginID, artifactID, operation, status, actor, message string, metadata any) error {
|
|
metadataJSON, err := marshalDefaultObject(metadata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_operations(plugin_id, artifact_id, operation, status, actor, message, metadata_json, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
pluginID, artifactID, operation, status, actor, message, metadataJSON, r.now().Unix())
|
|
return err
|
|
}
|
|
|
|
func (r Repository) ListOperations(ctx context.Context, pluginID string, limit int) ([]OperationRecord, error) {
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
query := `
|
|
SELECT id, plugin_id, artifact_id, operation, status, actor, message, metadata_json, created_at
|
|
FROM plugin_operations`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ?`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY created_at DESC, id DESC LIMIT ?`
|
|
args = append(args, limit)
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var records []OperationRecord
|
|
for rows.Next() {
|
|
var record OperationRecord
|
|
if err := rows.Scan(&record.ID, &record.PluginID, &record.ArtifactID, &record.Operation, &record.Status, &record.Actor, &record.Message, &record.MetadataJSON, &record.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
records = append(records, record)
|
|
}
|
|
return records, rows.Err()
|
|
}
|
|
|
|
func (r Repository) SaveEvent(ctx context.Context, event EventSummary, dropped bool, reason, traceID, connectionID string) error {
|
|
fields, err := marshalDefaultObject(event.Fields)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_events(plugin_id, name, fields_json, dropped, reason, trace_id, connection_id, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
event.PluginID, event.Name, fields, boolInt(dropped), reason, traceID, connectionID, r.now().Unix())
|
|
return err
|
|
}
|
|
|
|
func (r Repository) RecentEvents(ctx context.Context, pluginID string, limit int) ([]EventSummary, error) {
|
|
if limit <= 0 {
|
|
limit = DefaultEventRecentLimit
|
|
}
|
|
query := `SELECT plugin_id, name, fields_json, dropped, reason, created_at FROM plugin_events`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ?`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY created_at DESC, id DESC LIMIT ?`
|
|
args = append(args, limit)
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var events []EventSummary
|
|
for rows.Next() {
|
|
var event EventSummary
|
|
var fieldsJSON string
|
|
var dropped int
|
|
var reason string
|
|
if err := rows.Scan(&event.PluginID, &event.Name, &fieldsJSON, &dropped, &reason, &event.LastSeenAt); err != nil {
|
|
return nil, err
|
|
}
|
|
_ = json.Unmarshal([]byte(defaultJSONObject(fieldsJSON)), &event.Fields)
|
|
if dropped != 0 {
|
|
event.Dropped = 1
|
|
if reason != "" {
|
|
if event.Fields == nil {
|
|
event.Fields = make(map[string]string)
|
|
}
|
|
event.Fields["drop_reason"] = reason
|
|
}
|
|
} else {
|
|
event.Count = 1
|
|
}
|
|
events = append(events, event)
|
|
}
|
|
return events, rows.Err()
|
|
}
|
|
|
|
func (r Repository) SaveLog(ctx context.Context, log LogSummary) error {
|
|
fields, err := marshalDefaultObject(log.Fields)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_logs(plugin_id, level, message, fields_json, trace_id, connection_id, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
log.PluginID, log.Level, log.Message, fields, log.TraceID, log.ConnectionID, r.now().Unix())
|
|
return err
|
|
}
|
|
|
|
func (r Repository) RecentLogs(ctx context.Context, pluginID string, limit int) ([]LogSummary, error) {
|
|
if limit <= 0 {
|
|
limit = DefaultLogRecentLimit
|
|
}
|
|
query := `SELECT plugin_id, level, message, fields_json, trace_id, connection_id, created_at FROM plugin_logs`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ?`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY created_at DESC, id DESC LIMIT ?`
|
|
args = append(args, limit)
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var logs []LogSummary
|
|
for rows.Next() {
|
|
var item LogSummary
|
|
var fieldsJSON string
|
|
if err := rows.Scan(&item.PluginID, &item.Level, &item.Message, &fieldsJSON, &item.TraceID, &item.ConnectionID, &item.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
_ = json.Unmarshal([]byte(defaultJSONObject(fieldsJSON)), &item.Fields)
|
|
logs = append(logs, item)
|
|
}
|
|
return logs, rows.Err()
|
|
}
|
|
|
|
func (r Repository) SaveTrace(ctx context.Context, trace TraceSummary, fields map[string]string) error {
|
|
fieldsJSON, err := marshalDefaultObject(fields)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_traces(plugin_id, trace_id, connection_id, handler_id, operation, status, duration_ms, fields_json, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
trace.PluginID, trace.TraceID, trace.ConnectionID, trace.HandlerID, trace.Operation, trace.Status, trace.DurationMS, fieldsJSON, r.now().Unix())
|
|
return err
|
|
}
|
|
|
|
func (r Repository) RecentTraces(ctx context.Context, pluginID string, limit int) ([]TraceSummary, error) {
|
|
if limit <= 0 {
|
|
limit = 200
|
|
}
|
|
query := `SELECT plugin_id, trace_id, connection_id, handler_id, operation, status, duration_ms, created_at FROM plugin_traces`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ?`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY created_at DESC, id DESC LIMIT ?`
|
|
args = append(args, limit)
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var traces []TraceSummary
|
|
for rows.Next() {
|
|
var trace TraceSummary
|
|
if err := rows.Scan(&trace.PluginID, &trace.TraceID, &trace.ConnectionID, &trace.HandlerID, &trace.Operation, &trace.Status, &trace.DurationMS, &trace.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
traces = append(traces, trace)
|
|
}
|
|
return traces, rows.Err()
|
|
}
|
|
|
|
func (r Repository) PutPluginData(ctx context.Context, record PluginDataSummary, value []byte) error {
|
|
now := r.now().Unix()
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_data(plugin_id, key, value, schema_version, data_class, exportable, size_bytes, expires_at, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(plugin_id, key) DO UPDATE SET
|
|
value = excluded.value,
|
|
schema_version = excluded.schema_version,
|
|
data_class = excluded.data_class,
|
|
exportable = excluded.exportable,
|
|
size_bytes = excluded.size_bytes,
|
|
expires_at = excluded.expires_at,
|
|
updated_at = excluded.updated_at`,
|
|
record.PluginID, record.Key, value, record.SchemaVersion, record.DataClass, boolInt(record.Exportable), int64(len(value)), record.ExpiresAt, now, now)
|
|
return err
|
|
}
|
|
|
|
func (r Repository) GetPluginData(ctx context.Context, pluginID, key string) (PluginDataSummary, []byte, error) {
|
|
row := r.db.QueryRowContext(ctx, `
|
|
SELECT plugin_id, key, value, schema_version, data_class, exportable, size_bytes, expires_at, updated_at
|
|
FROM plugin_data WHERE plugin_id = ? AND key = ?`, pluginID, key)
|
|
var record PluginDataSummary
|
|
var exportable int
|
|
var value []byte
|
|
err := row.Scan(&record.PluginID, &record.Key, &value, &record.SchemaVersion, &record.DataClass, &exportable, &record.SizeBytes, &record.ExpiresAt, &record.UpdatedAt)
|
|
record.Exportable = exportable != 0
|
|
return record, value, err
|
|
}
|
|
|
|
func (r Repository) DeletePluginData(ctx context.Context, pluginID, key string) error {
|
|
_, err := r.db.ExecContext(ctx, `DELETE FROM plugin_data WHERE plugin_id = ? AND key = ?`, pluginID, key)
|
|
return err
|
|
}
|
|
|
|
func (r Repository) ListPluginData(ctx context.Context, pluginID string) ([]PluginDataSummary, error) {
|
|
query := `SELECT plugin_id, key, schema_version, data_class, exportable, size_bytes, expires_at, updated_at FROM plugin_data`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ?`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY updated_at DESC, key ASC`
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var records []PluginDataSummary
|
|
for rows.Next() {
|
|
var record PluginDataSummary
|
|
var exportable int
|
|
if err := rows.Scan(&record.PluginID, &record.Key, &record.SchemaVersion, &record.DataClass, &exportable, &record.SizeBytes, &record.ExpiresAt, &record.UpdatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
record.Exportable = exportable != 0
|
|
records = append(records, record)
|
|
}
|
|
return records, rows.Err()
|
|
}
|
|
|
|
func (r Repository) PluginDataUsage(ctx context.Context, pluginID string) (int64, error) {
|
|
row := r.db.QueryRowContext(ctx, `SELECT COALESCE(SUM(size_bytes), 0) FROM plugin_data WHERE plugin_id = ?`, pluginID)
|
|
var total int64
|
|
return total, row.Scan(&total)
|
|
}
|
|
|
|
func (r Repository) UpsertPluginFile(ctx context.Context, record PluginFileSummary, diskPath string) error {
|
|
now := r.now().Unix()
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_files(plugin_id, namespace, path, disk_path, data_class, exportable, readonly, size_bytes, expires_at, created_at, updated_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(plugin_id, namespace, path) DO UPDATE SET
|
|
disk_path = excluded.disk_path,
|
|
data_class = excluded.data_class,
|
|
exportable = excluded.exportable,
|
|
readonly = excluded.readonly,
|
|
size_bytes = excluded.size_bytes,
|
|
expires_at = excluded.expires_at,
|
|
updated_at = excluded.updated_at`,
|
|
record.PluginID, record.Namespace, record.Path, diskPath, record.DataClass, boolInt(record.Readonly), boolInt(record.Readonly), record.SizeBytes, record.ExpiresAt, now, now)
|
|
return err
|
|
}
|
|
|
|
func (r Repository) DeletePluginFile(ctx context.Context, pluginID, namespace, name string) error {
|
|
_, err := r.db.ExecContext(ctx, `DELETE FROM plugin_files WHERE plugin_id = ? AND namespace = ? AND path = ?`, pluginID, namespace, name)
|
|
return err
|
|
}
|
|
|
|
func (r Repository) ListPluginFiles(ctx context.Context, pluginID string) ([]PluginFileSummary, error) {
|
|
query := `SELECT plugin_id, namespace, path, data_class, size_bytes, expires_at, updated_at, readonly FROM plugin_files`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ?`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY updated_at DESC, namespace ASC, path ASC`
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var records []PluginFileSummary
|
|
for rows.Next() {
|
|
var record PluginFileSummary
|
|
var readonly int
|
|
if err := rows.Scan(&record.PluginID, &record.Namespace, &record.Path, &record.DataClass, &record.SizeBytes, &record.ExpiresAt, &record.UpdatedAt, &readonly); err != nil {
|
|
return nil, err
|
|
}
|
|
record.Readonly = readonly != 0
|
|
records = append(records, record)
|
|
}
|
|
return records, rows.Err()
|
|
}
|
|
|
|
func (r Repository) PluginFileUsage(ctx context.Context, pluginID string) (int64, error) {
|
|
row := r.db.QueryRowContext(ctx, `SELECT COALESCE(SUM(size_bytes), 0) FROM plugin_files WHERE plugin_id = ?`, pluginID)
|
|
var total int64
|
|
return total, row.Scan(&total)
|
|
}
|
|
|
|
func (r Repository) SaveDiagnostic(ctx context.Context, pluginID, path string, size int64, sections []string) (DiagnosticPackageSummary, error) {
|
|
sectionsJSON, err := marshalDefaultObject(sections)
|
|
if err != nil {
|
|
return DiagnosticPackageSummary{}, err
|
|
}
|
|
now := r.now().Unix()
|
|
result, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO plugin_diagnostics(plugin_id, path, size_bytes, sections_json, created_at)
|
|
VALUES (?, ?, ?, ?, ?)`, pluginID, path, size, sectionsJSON, now)
|
|
if err != nil {
|
|
return DiagnosticPackageSummary{}, err
|
|
}
|
|
_, _ = result.LastInsertId()
|
|
return DiagnosticPackageSummary{PluginID: pluginID, CreatedAt: now, SizeBytes: size, Sections: sections}, nil
|
|
}
|
|
|
|
func (r Repository) ListDiagnostics(ctx context.Context, pluginID string, limit int) ([]DiagnosticPackageSummary, error) {
|
|
if limit <= 0 {
|
|
limit = 20
|
|
}
|
|
query := `SELECT plugin_id, size_bytes, sections_json, created_at FROM plugin_diagnostics`
|
|
var args []any
|
|
if pluginID != "" {
|
|
query += ` WHERE plugin_id = ?`
|
|
args = append(args, pluginID)
|
|
}
|
|
query += ` ORDER BY created_at DESC, id DESC LIMIT ?`
|
|
args = append(args, limit)
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var records []DiagnosticPackageSummary
|
|
for rows.Next() {
|
|
var record DiagnosticPackageSummary
|
|
var sectionsJSON string
|
|
if err := rows.Scan(&record.PluginID, &record.SizeBytes, §ionsJSON, &record.CreatedAt); err != nil {
|
|
return nil, err
|
|
}
|
|
_ = json.Unmarshal([]byte(defaultJSONArray(sectionsJSON)), &record.Sections)
|
|
records = append(records, record)
|
|
}
|
|
return records, rows.Err()
|
|
}
|
|
|
|
func (r Repository) DispatchPlan(ctx context.Context) (DispatchPlan, error) {
|
|
plugins, err := r.ListPlugins(ctx)
|
|
if err != nil {
|
|
return DispatchPlan{}, err
|
|
}
|
|
plan := DispatchPlan{UpdatedAt: r.now().Unix()}
|
|
for _, plugin := range plugins {
|
|
if plugin.DispatchSummaryJSON == "" || plugin.RuntimeState != RuntimeEnabled {
|
|
continue
|
|
}
|
|
var summaries []DispatchHandlerSummary
|
|
if err := json.Unmarshal([]byte(plugin.DispatchSummaryJSON), &summaries); err == nil {
|
|
plan.Handlers = append(plan.Handlers, summaries...)
|
|
}
|
|
}
|
|
return plan, nil
|
|
}
|
|
|
|
type rowScanner interface {
|
|
Scan(dest ...any) error
|
|
}
|
|
|
|
func scanPluginServiceState(row rowScanner) (PluginServiceState, error) {
|
|
var state PluginServiceState
|
|
err := row.Scan(
|
|
&state.DesiredMode, &state.ActiveMode, &state.AppliedAt, &state.LiveMigration,
|
|
&state.LastError, &state.UpdatedBy, &state.UpdatedAt,
|
|
)
|
|
state = normalizePluginServiceState(state)
|
|
return state, err
|
|
}
|
|
|
|
func scanArtifact(row rowScanner) (ArtifactRecord, error) {
|
|
var artifact ArtifactRecord
|
|
err := row.Scan(
|
|
&artifact.ID, &artifact.PluginID, &artifact.Version, &artifact.FileName, &artifact.FilePath, &artifact.SHA256, &artifact.PackageSHA256, &artifact.SizeBytes,
|
|
&artifact.ArtifactType, &artifact.RuntimeType, &artifact.RuntimeEntry, &artifact.Status, &artifact.MetadataJSON,
|
|
&artifact.CapabilitiesSummaryJSON, &artifact.ExtensionPointsJSON, &artifact.APIVersion, &artifact.GoVersion, &artifact.GOOS, &artifact.GOARCH,
|
|
&artifact.UploadedBy, &artifact.Error, &artifact.CreatedAt, &artifact.UpdatedAt,
|
|
)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return ArtifactRecord{}, ErrArtifactNotFound
|
|
}
|
|
return artifact, err
|
|
}
|
|
|
|
func scanPluginRow(row rowScanner, plugin *PluginRecord) error {
|
|
return row.Scan(
|
|
&plugin.ID, &plugin.DesiredArtifactID, &plugin.ActiveArtifactID, &plugin.LoadedArtifactID, &plugin.DesiredState, &plugin.RuntimeState,
|
|
&plugin.Priority, &plugin.ConfigJSON, &plugin.DesiredGeneration, &plugin.AppliedGeneration, &plugin.LastError,
|
|
&plugin.RuntimeSummaryJSON, &plugin.DispatchSummaryJSON, &plugin.CreatedAt, &plugin.UpdatedAt, &plugin.UpdatedBy,
|
|
)
|
|
}
|
|
|
|
func scanConfigSnapshot(row rowScanner) (ConfigSnapshotRecord, error) {
|
|
var snapshot ConfigSnapshotRecord
|
|
err := row.Scan(
|
|
&snapshot.ID, &snapshot.PluginID, &snapshot.ArtifactID, &snapshot.ConfigJSON, &snapshot.DesiredState,
|
|
&snapshot.Priority, &snapshot.DesiredGeneration, &snapshot.CreatedBy, &snapshot.CreatedAt,
|
|
)
|
|
return snapshot, err
|
|
}
|
|
|
|
func scanSecret(row rowScanner) (SecretRecord, error) {
|
|
var secret SecretRecord
|
|
var reloadRequired int
|
|
var hotReload int
|
|
err := row.Scan(
|
|
&secret.PluginID, &secret.Name, &secret.CurrentVersion, &secret.PreviousVersion,
|
|
&reloadRequired, &hotReload, &secret.UpdatedBy, &secret.CreatedAt, &secret.UpdatedAt,
|
|
)
|
|
secret.ReloadRequired = reloadRequired != 0
|
|
secret.HotReload = hotReload != 0
|
|
return secret, err
|
|
}
|
|
|
|
func scanBuild(row rowScanner) (BuildRecord, error) {
|
|
var build BuildRecord
|
|
var vendorRequired int
|
|
err := row.Scan(
|
|
&build.ID, &build.PluginID, &build.SourceID, &build.ArtifactID, &build.Status, &build.BuilderType, &build.BuilderImage, &build.BuilderVersion,
|
|
&build.GoVersion, &build.GOOS, &build.GOARCH, &build.GOAMD64, &build.GOARM64, &build.CGOEnabled, &build.BuildTags,
|
|
&build.SDKModule, &build.SDKVersion, &build.GOPROXY, &build.GONOSUMDB, &build.GOPRIVATE, &vendorRequired,
|
|
&build.SourceSHA256, &build.ArtifactSHA256, &build.ModuleSummary, &build.GoVersionM, &build.ABIFingerprint,
|
|
&build.LogSummary, &build.MetadataJSON, &build.Error, &build.StartedAt, &build.EndedAt, &build.DurationMS, &build.CreatedBy, &build.CreatedAt, &build.UpdatedAt,
|
|
)
|
|
build.VendorRequired = vendorRequired != 0
|
|
return build, err
|
|
}
|
|
|
|
func scanReview(row rowScanner) (ReviewRecord, error) {
|
|
var review ReviewRecord
|
|
err := row.Scan(
|
|
&review.ID, &review.PluginID, &review.ArtifactID, &review.Profile, &review.RiskLevel,
|
|
&review.ConfigHash, &review.ScopeHash, &review.RolloutHash, &review.RuntimeLimitsHash,
|
|
&review.FeaturesHash, &review.PolicyHash, &review.Decision, &review.Notes, &review.ReviewedBy, &review.CreatedAt,
|
|
)
|
|
return review, err
|
|
}
|
|
|
|
func scanWarningOverride(row rowScanner) (WarningOverrideRecord, error) {
|
|
var override WarningOverrideRecord
|
|
err := row.Scan(
|
|
&override.ID, &override.PluginID, &override.ArtifactID, &override.Profile, &override.Action,
|
|
&override.PolicyHash, &override.Reason, &override.CreatedBy, &override.ExpiresAt, &override.CreatedAt,
|
|
)
|
|
return override, err
|
|
}
|
|
|
|
func scanAdvisory(row rowScanner) (AdvisoryRecord, error) {
|
|
var advisory AdvisoryRecord
|
|
err := row.Scan(
|
|
&advisory.ID, &advisory.AdvisoryID, &advisory.Status, &advisory.Action, &advisory.ArtifactSHA256,
|
|
&advisory.PluginID, &advisory.VersionRange, &advisory.DependencyName, &advisory.DependencyRange,
|
|
&advisory.RecommendedAction, &advisory.FixedVersion, &advisory.Mitigation, &advisory.CreatedBy,
|
|
&advisory.CreatedAt, &advisory.UpdatedAt,
|
|
)
|
|
return advisory, err
|
|
}
|
|
|
|
func scanPreflight(row rowScanner) (PreflightRecord, error) {
|
|
var record PreflightRecord
|
|
err := row.Scan(
|
|
&record.ID, &record.PluginID, &record.ArtifactID, &record.Profile, &record.Status,
|
|
&record.ResultJSON, &record.CreatedBy, &record.CreatedAt,
|
|
)
|
|
return record, err
|
|
}
|
|
|
|
func scanBenchmark(row rowScanner) (BenchmarkRecord, error) {
|
|
var record BenchmarkRecord
|
|
err := row.Scan(
|
|
&record.ID, &record.PluginID, &record.ArtifactID, &record.Profile, &record.BenchmarkProfile,
|
|
&record.P95MS, &record.P99MS, &record.ErrorRate, &record.ActiveProxyCapacity,
|
|
&record.BaselineDiff, &record.CreatedBy, &record.CreatedAt,
|
|
)
|
|
return record, err
|
|
}
|
|
|
|
func scanRepositoryImport(row rowScanner) (RepositoryImportRecord, error) {
|
|
var record RepositoryImportRecord
|
|
err := row.Scan(
|
|
&record.ID, &record.RepositoryType, &record.IndexPath, &record.RepositoryName,
|
|
&record.CandidateID, &record.PluginID, &record.Version, &record.ArtifactID,
|
|
&record.PackageSHA256, &record.TrustPolicy, &record.AdmissionJSON,
|
|
&record.ImportedBy, &record.CreatedAt,
|
|
)
|
|
return record, err
|
|
}
|
|
|
|
func scanSupplyChainAssessment(row rowScanner) (SupplyChainAssessment, error) {
|
|
var assessment SupplyChainAssessment
|
|
var issuesJSON, signatureJSON, sbomJSON, licenseJSON, advisoryJSON, metadataJSON string
|
|
err := row.Scan(
|
|
&assessment.ID, &assessment.PluginID, &assessment.ArtifactID, &assessment.Status,
|
|
&issuesJSON, &signatureJSON, &sbomJSON, &licenseJSON, &advisoryJSON,
|
|
&metadataJSON, &assessment.CreatedBy, &assessment.CreatedAt,
|
|
)
|
|
if err != nil {
|
|
return assessment, err
|
|
}
|
|
_ = json.Unmarshal([]byte(defaultJSONArray(issuesJSON)), &assessment.Issues)
|
|
assessment.Signature = jsonMap(signatureJSON)
|
|
assessment.SBOM = jsonMap(sbomJSON)
|
|
assessment.License = jsonMap(licenseJSON)
|
|
assessment.Advisory = jsonMap(advisoryJSON)
|
|
assessment.Metadata = jsonMap(metadataJSON)
|
|
return assessment, nil
|
|
}
|
|
|
|
func scanInstrumentation(row rowScanner) (InstrumentationRecord, error) {
|
|
var record InstrumentationRecord
|
|
err := row.Scan(
|
|
&record.ID, &record.Name, &record.Version, &record.Profile, &record.GeneratedDiffHash,
|
|
&record.ProvenanceJSON, &record.ConformanceJSON, &record.BenchmarkJSON,
|
|
&record.SmokeJSON, &record.RunbookRollback, &record.Status, &record.CreatedBy,
|
|
&record.CreatedAt,
|
|
)
|
|
return record, err
|
|
}
|
|
|
|
func marshalDefaultObject(value any) (string, error) {
|
|
if value == nil {
|
|
return "{}", nil
|
|
}
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(data) == 0 || string(data) == "null" {
|
|
return "{}", nil
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
func defaultJSONObject(value string) string {
|
|
if value == "" || !json.Valid([]byte(value)) {
|
|
return "{}"
|
|
}
|
|
return value
|
|
}
|
|
|
|
func defaultJSONArray(value string) string {
|
|
if value == "" || !json.Valid([]byte(value)) {
|
|
return "[]"
|
|
}
|
|
return value
|
|
}
|
|
|
|
func jsonMap(value string) map[string]any {
|
|
var out map[string]any
|
|
if json.Unmarshal([]byte(defaultJSONObject(value)), &out) != nil {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
func normalizePluginServiceState(state PluginServiceState) PluginServiceState {
|
|
if state.DesiredMode == "" {
|
|
state.DesiredMode = PluginServiceModeInProcess
|
|
}
|
|
if state.ActiveMode == "" {
|
|
state.ActiveMode = PluginServiceModeInProcess
|
|
}
|
|
if state.LiveMigration == "" {
|
|
state.LiveMigration = PluginMigrationDrainOnly
|
|
}
|
|
state.RestartRequired = state.DesiredMode != state.ActiveMode
|
|
return state
|
|
}
|
|
|
|
func boolInt(value bool) int {
|
|
if value {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func lastInsertID(ctx context.Context, db *sql.DB) (int64, error) {
|
|
row := db.QueryRowContext(ctx, `SELECT last_insert_rowid()`)
|
|
var id int64
|
|
if err := row.Scan(&id); err != nil {
|
|
return 0, err
|
|
}
|
|
return id, nil
|
|
}
|