Add third-party service storage support
This commit is contained in:
parent
951b528f97
commit
5620aad10b
@ -68,6 +68,15 @@ type ConfigOverlayAsset struct {
|
||||
Raw map[string]any `json:"raw"`
|
||||
}
|
||||
|
||||
type ConfigIntegrationServiceAsset struct {
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Raw map[string]any `json:"raw"`
|
||||
}
|
||||
|
||||
func (s *ConfigPreviewService) ListTemplateAssets() ([]ConfigTemplateAsset, error) {
|
||||
items := make([]ConfigTemplateAsset, 0)
|
||||
seen := map[string]bool{}
|
||||
@ -320,6 +329,42 @@ func (s *ConfigPreviewService) GetOverlayAsset(name string) (*ConfigOverlayAsset
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ConfigPreviewService) ListIntegrationServices() ([]ConfigIntegrationServiceAsset, error) {
|
||||
if s == nil || s.assets == nil {
|
||||
return nil, nil
|
||||
}
|
||||
records, err := s.assets.ListIntegrationServices()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]ConfigIntegrationServiceAsset, 0, len(records))
|
||||
for _, record := range records {
|
||||
item, err := integrationServiceAssetFromRecord(record)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, *item)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *ConfigPreviewService) GetIntegrationService(name string) (*ConfigIntegrationServiceAsset, error) {
|
||||
if s == nil || s.assets == nil {
|
||||
return nil, fmt.Errorf("asset repository is not configured")
|
||||
}
|
||||
if err := validateConfigName(name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
record, err := s.assets.GetIntegrationService(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if record == nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return integrationServiceAssetFromRecord(*record)
|
||||
}
|
||||
|
||||
func (s *ConfigPreviewService) readAssetJSON(kind string, name string) (map[string]any, string, error) {
|
||||
if s != nil && s.assets != nil {
|
||||
raw, path, ok, err := s.readRepoAssetJSON(kind, name)
|
||||
@ -468,6 +513,24 @@ func buildTemplateAsset(raw map[string]any, path string, origin string, readOnly
|
||||
}
|
||||
}
|
||||
|
||||
func integrationServiceAssetFromRecord(record storage.IntegrationServiceRecord) (*ConfigIntegrationServiceAsset, error) {
|
||||
var raw map[string]any
|
||||
if err := json.Unmarshal([]byte(record.BodyJSON), &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if raw == nil {
|
||||
raw = map[string]any{}
|
||||
}
|
||||
return &ConfigIntegrationServiceAsset{
|
||||
Name: firstString(raw["name"], record.Name),
|
||||
Path: repoAssetPath("integration_services", record.Name),
|
||||
Type: firstString(record.ServiceType, stringValue(raw["type"])),
|
||||
Description: firstString(raw["description"], record.Description),
|
||||
Enabled: boolValue(raw["enabled"], record.Enabled),
|
||||
Raw: raw,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ConfigPreviewService) mediaAssetPath(kind string, name string) (string, bool) {
|
||||
root := s.mediaRepoRoot()
|
||||
if root == "" {
|
||||
@ -519,6 +582,24 @@ func valueString(v any) string {
|
||||
}
|
||||
}
|
||||
|
||||
func boolValue(v any, fallback bool) bool {
|
||||
switch value := v.(type) {
|
||||
case bool:
|
||||
return value
|
||||
case float64:
|
||||
return value != 0
|
||||
case int:
|
||||
return value != 0
|
||||
case int64:
|
||||
return value != 0
|
||||
case string:
|
||||
value = strings.TrimSpace(strings.ToLower(value))
|
||||
return value == "1" || value == "true" || value == "yes" || value == "on"
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
func firstString(v any, fallback string) string {
|
||||
if got := stringValue(v); got != "" {
|
||||
return got
|
||||
|
||||
@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -336,6 +337,111 @@ func TestConfigPreviewServiceListsSourcesFromAssetsRepo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListIntegrationServices(t *testing.T) {
|
||||
store, err := storage.OpenSQLite(filepath.Join(t.TempDir(), "app.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("OpenSQLite: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
repo := storage.NewAssetsRepo(store.DB())
|
||||
if err := repo.SaveIntegrationService(
|
||||
"minio_primary",
|
||||
"object_storage",
|
||||
"primary object store",
|
||||
false,
|
||||
`{"name":"minio_primary","type":"object_storage","provider":"minio","endpoint":"http://10.0.0.49:9000","enabled":false}`,
|
||||
); err != nil {
|
||||
t.Fatalf("SaveIntegrationService: %v", err)
|
||||
}
|
||||
|
||||
svc := NewConfigPreviewService(&config.Config{}, repo)
|
||||
items, err := svc.ListIntegrationServices()
|
||||
if err != nil {
|
||||
t.Fatalf("ListIntegrationServices: %v", err)
|
||||
}
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("expected 1 integration service, got %#v", items)
|
||||
}
|
||||
if items[0].Name != "minio_primary" || items[0].Type != "object_storage" || items[0].Enabled {
|
||||
t.Fatalf("unexpected integration service summary: %#v", items[0])
|
||||
}
|
||||
|
||||
item, err := svc.GetIntegrationService("minio_primary")
|
||||
if err != nil {
|
||||
t.Fatalf("GetIntegrationService: %v", err)
|
||||
}
|
||||
if item == nil {
|
||||
t.Fatal("expected integration service")
|
||||
}
|
||||
if item.Description != "primary object store" {
|
||||
t.Fatalf("unexpected integration service description: %#v", item)
|
||||
}
|
||||
if item.Type != "object_storage" || item.Enabled {
|
||||
t.Fatalf("unexpected integration service status: %#v", item)
|
||||
}
|
||||
if got := stringValue(item.Raw["type"]); got != "object_storage" {
|
||||
t.Fatalf("expected raw type to come from body_json, got %#v", item.Raw)
|
||||
}
|
||||
if got := stringValue(item.Raw["provider"]); got != "minio" {
|
||||
t.Fatalf("unexpected integration service provider: %#v", item.Raw)
|
||||
}
|
||||
if got := stringValue(item.Raw["endpoint"]); got != "http://10.0.0.49:9000" {
|
||||
t.Fatalf("unexpected integration service endpoint: %#v", item.Raw)
|
||||
}
|
||||
if enabled, ok := item.Raw["enabled"].(bool); !ok || enabled {
|
||||
t.Fatalf("expected raw enabled=false, got %#v", item.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetIntegrationServiceNotFound(t *testing.T) {
|
||||
store, err := storage.OpenSQLite(filepath.Join(t.TempDir(), "app.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("OpenSQLite: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
svc := NewConfigPreviewService(&config.Config{}, storage.NewAssetsRepo(store.DB()))
|
||||
item, err := svc.GetIntegrationService("missing_service")
|
||||
if !strings.Contains(err.Error(), "file does not exist") && !os.IsNotExist(err) {
|
||||
t.Fatalf("expected not found error, got item=%#v err=%v", item, err)
|
||||
}
|
||||
if item != nil {
|
||||
t.Fatalf("expected nil item for missing integration service, got %#v", item)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetIntegrationServicePrefersRecordTypeOverRawType(t *testing.T) {
|
||||
store, err := storage.OpenSQLite(filepath.Join(t.TempDir(), "app.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("OpenSQLite: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
repo := storage.NewAssetsRepo(store.DB())
|
||||
if err := repo.SaveIntegrationService(
|
||||
"minio_primary",
|
||||
"object_storage",
|
||||
"primary object store",
|
||||
true,
|
||||
`{"name":"minio_primary","type":"minio","provider":"minio","enabled":true}`,
|
||||
); err != nil {
|
||||
t.Fatalf("SaveIntegrationService: %v", err)
|
||||
}
|
||||
|
||||
svc := NewConfigPreviewService(&config.Config{}, repo)
|
||||
item, err := svc.GetIntegrationService("minio_primary")
|
||||
if err != nil {
|
||||
t.Fatalf("GetIntegrationService: %v", err)
|
||||
}
|
||||
if item.Type != "object_storage" {
|
||||
t.Fatalf("expected canonical type from record, got %#v", item)
|
||||
}
|
||||
if got := stringValue(item.Raw["type"]); got != "minio" {
|
||||
t.Fatalf("expected raw type to preserve original body_json, got %#v", item.Raw)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigPreviewServiceSavesProfileEditorToAssetsRepo(t *testing.T) {
|
||||
store, err := storage.OpenSQLite(filepath.Join(t.TempDir(), "app.db"))
|
||||
if err != nil {
|
||||
|
||||
@ -18,6 +18,16 @@ type AssetRecord struct {
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
type IntegrationServiceRecord struct {
|
||||
Name string
|
||||
ServiceType string
|
||||
Description string
|
||||
Enabled bool
|
||||
BodyJSON string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
type AssetsRepo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
@ -52,6 +62,24 @@ func (r *AssetsRepo) SaveOverlay(name string, description string, bodyJSON strin
|
||||
})
|
||||
}
|
||||
|
||||
func (r *AssetsRepo) SaveIntegrationService(name string, serviceType string, description string, enabled bool, bodyJSON string) error {
|
||||
if r == nil || r.db == nil {
|
||||
return nil
|
||||
}
|
||||
now := time.Now().Format(time.RFC3339)
|
||||
_, err := r.db.Exec(`
|
||||
INSERT INTO integration_services(name, type, description, enabled, body_json, created_at, updated_at)
|
||||
VALUES(?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM integration_services WHERE name = ?), ?), ?)
|
||||
ON CONFLICT(name) DO UPDATE SET
|
||||
type=excluded.type,
|
||||
description=excluded.description,
|
||||
enabled=excluded.enabled,
|
||||
body_json=excluded.body_json,
|
||||
updated_at=excluded.updated_at
|
||||
`, name, serviceType, description, enabled, bodyJSON, name, now, now)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *AssetsRepo) ListTemplates() ([]AssetRecord, error) {
|
||||
return r.listAssets("templates")
|
||||
}
|
||||
@ -64,6 +92,31 @@ func (r *AssetsRepo) ListOverlays() ([]AssetRecord, error) {
|
||||
return r.listAssets("overlays")
|
||||
}
|
||||
|
||||
func (r *AssetsRepo) ListIntegrationServices() ([]IntegrationServiceRecord, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := r.db.Query(`
|
||||
SELECT name, type, description, enabled, body_json, created_at, updated_at
|
||||
FROM integration_services
|
||||
ORDER BY updated_at DESC, name ASC
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var out []IntegrationServiceRecord
|
||||
for rows.Next() {
|
||||
var item IntegrationServiceRecord
|
||||
if err := rows.Scan(&item.Name, &item.ServiceType, &item.Description, &item.Enabled, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (r *AssetsRepo) GetTemplate(name string) (*AssetRecord, error) {
|
||||
return r.getAsset("templates", name)
|
||||
}
|
||||
@ -76,10 +129,37 @@ func (r *AssetsRepo) GetOverlay(name string) (*AssetRecord, error) {
|
||||
return r.getAsset("overlays", name)
|
||||
}
|
||||
|
||||
func (r *AssetsRepo) GetIntegrationService(name string) (*IntegrationServiceRecord, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var item IntegrationServiceRecord
|
||||
err := r.db.QueryRow(`
|
||||
SELECT name, type, description, enabled, body_json, created_at, updated_at
|
||||
FROM integration_services
|
||||
WHERE name = ?
|
||||
`, name).Scan(&item.Name, &item.ServiceType, &item.Description, &item.Enabled, &item.BodyJSON, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (r *AssetsRepo) DeleteTemplate(name string) error {
|
||||
return r.deleteAsset("templates", name)
|
||||
}
|
||||
|
||||
func (r *AssetsRepo) DeleteIntegrationService(name string) error {
|
||||
if r == nil || r.db == nil {
|
||||
return nil
|
||||
}
|
||||
_, err := r.db.Exec(`DELETE FROM integration_services WHERE name = ?`, name)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *AssetsRepo) RenameTemplate(oldName string, newName string, description string, bodyJSON string) error {
|
||||
if r == nil || r.db == nil {
|
||||
return nil
|
||||
|
||||
@ -102,3 +102,44 @@ func TestAssetsRepoDeleteTemplateRemovesRecord(t *testing.T) {
|
||||
t.Fatalf("expected template deleted, got %#v", record)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssetsRepoStoresIntegrationServiceDisabledState(t *testing.T) {
|
||||
store := openTestStore(t)
|
||||
defer store.Close()
|
||||
|
||||
repo := NewAssetsRepo(store.DB())
|
||||
if err := repo.SaveIntegrationService(
|
||||
"minio_primary",
|
||||
"object_storage",
|
||||
"primary object store",
|
||||
false,
|
||||
`{"name":"minio_primary","type":"object_storage","provider":"minio","enabled":false}`,
|
||||
); err != nil {
|
||||
t.Fatalf("SaveIntegrationService: %v", err)
|
||||
}
|
||||
|
||||
records, err := repo.ListIntegrationServices()
|
||||
if err != nil {
|
||||
t.Fatalf("ListIntegrationServices: %v", err)
|
||||
}
|
||||
if len(records) != 1 {
|
||||
t.Fatalf("expected one integration service, got %#v", records)
|
||||
}
|
||||
if records[0].ServiceType != "object_storage" || records[0].Enabled {
|
||||
t.Fatalf("unexpected integration service record: %#v", records[0])
|
||||
}
|
||||
|
||||
record, err := repo.GetIntegrationService("minio_primary")
|
||||
if err != nil {
|
||||
t.Fatalf("GetIntegrationService: %v", err)
|
||||
}
|
||||
if record == nil {
|
||||
t.Fatal("expected integration service record")
|
||||
}
|
||||
if record.ServiceType != "object_storage" || record.Enabled {
|
||||
t.Fatalf("unexpected integration service fetch: %#v", record)
|
||||
}
|
||||
if !strings.Contains(record.BodyJSON, `"provider":"minio"`) {
|
||||
t.Fatalf("expected body_json payload preserved, got %#v", record)
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,6 +29,16 @@ CREATE TABLE IF NOT EXISTS overlays (
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS integration_services (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
type TEXT NOT NULL DEFAULT '',
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
enabled INTEGER NOT NULL DEFAULT 0,
|
||||
body_json TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS devices (
|
||||
device_id TEXT PRIMARY KEY,
|
||||
hostname TEXT NOT NULL DEFAULT '',
|
||||
|
||||
@ -17,6 +17,7 @@ func TestSQLiteStoreBootstrapsSchema(t *testing.T) {
|
||||
"templates",
|
||||
"profiles",
|
||||
"overlays",
|
||||
"integration_services",
|
||||
"devices",
|
||||
"device_config_state",
|
||||
"tasks",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user