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
Go / merge-artifacts (push) Has been cancelled
Docker Image / docker (push) Has been cancelled
338 lines
12 KiB
Go
338 lines
12 KiB
Go
package pluginmanager
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"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")
|
|
}
|
|
|
|
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) 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) 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) 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) 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) 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 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 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
|
|
}
|