Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ RUN apk add --no-cache git ca-certificates tzdata
WORKDIR /build

# Copy module manifests first for better layer caching.
COPY go.work go.work.sum ./
COPY go.mod go.sum ./
# GOWORK=off disables go.work so go.mod is used directly — all dependencies
# are fetched from the module proxy using their published versions.
COPY go.mod go.sum go.work go.work.sum ./
COPY packages/go.mod packages/

RUN go work sync
RUN GOWORK=off go mod download

# Copy full source and build.
COPY . .

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
GOWORK=off \
go build \
-ldflags="-w -s" \
-trimpath \
Expand Down
3 changes: 3 additions & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ use (
// Platform control plane API (modular monolith)
.

// Plugin SDK (local source — repo renamed to plugin-sdk-go)
../plugin-sdk-go

// Shared Go library
./packages
)
2 changes: 2 additions & 0 deletions internal/core/plugins/adapters/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ type pluginResponse struct {
Image string `json:"image"`
Version string `json:"version"`
GRPCAddr string `json:"grpc_addr"`
FrontendURL string `json:"frontend_url"`
Config json.RawMessage `json:"config"`
Enabled bool `json:"enabled"`
Status plugindomain.PluginStatus `json:"status"`
Expand All @@ -259,6 +260,7 @@ func toResponse(p *plugindomain.Plugin) pluginResponse {
Image: p.Image,
Version: p.Version,
GRPCAddr: p.GRPCAddr,
FrontendURL: p.FrontendURL,
Config: p.Config, // secrets already stripped from Config field
Enabled: p.Enabled,
Status: p.Status,
Expand Down
42 changes: 26 additions & 16 deletions internal/core/plugins/adapters/persistence/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (s *PostgresPluginStore) FindByID(ctx context.Context, id string) (*plugind
return nil, domain.ErrNotFound
}
const q = `
SELECT id, type, display_name, image, version, grpc_addr,
SELECT id, type, display_name, image, version, grpc_addr, frontend_url,
config, secrets, enabled, installed_at, updated_at
FROM plugins WHERE id = $1`
return scanPlugin(s.db.QueryRowContext(ctx, q, id))
Expand All @@ -44,7 +44,7 @@ func (s *PostgresPluginStore) ListAll(ctx context.Context) ([]*plugindomain.Plug
return nil, nil
}
const q = `
SELECT id, type, display_name, image, version, grpc_addr,
SELECT id, type, display_name, image, version, grpc_addr, frontend_url,
config, secrets, enabled, installed_at, updated_at
FROM plugins ORDER BY installed_at DESC`
rows, err := s.db.QueryContext(ctx, q)
Expand All @@ -60,7 +60,7 @@ func (s *PostgresPluginStore) ListByType(ctx context.Context, pluginType string)
return nil, nil
}
const q = `
SELECT id, type, display_name, image, version, grpc_addr,
SELECT id, type, display_name, image, version, grpc_addr, frontend_url,
config, secrets, enabled, installed_at, updated_at
FROM plugins WHERE type = $1 ORDER BY installed_at DESC`
rows, err := s.db.QueryContext(ctx, q, pluginType)
Expand All @@ -77,21 +77,27 @@ func (s *PostgresPluginStore) Save(ctx context.Context, p *plugindomain.Plugin)
}
const q = `
INSERT INTO plugins
(id, type, display_name, image, version, grpc_addr,
(id, type, display_name, image, version, grpc_addr, frontend_url,
config, secrets, enabled, installed_at, updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
ON CONFLICT (id) DO UPDATE SET
display_name = EXCLUDED.display_name,
image = EXCLUDED.image,
version = EXCLUDED.version,
grpc_addr = EXCLUDED.grpc_addr,
config = EXCLUDED.config,
secrets = EXCLUDED.secrets,
enabled = EXCLUDED.enabled,
updated_at = EXCLUDED.updated_at`
display_name = EXCLUDED.display_name,
image = EXCLUDED.image,
version = EXCLUDED.version,
grpc_addr = EXCLUDED.grpc_addr,
frontend_url = EXCLUDED.frontend_url,
config = EXCLUDED.config,
secrets = EXCLUDED.secrets,
enabled = EXCLUDED.enabled,
updated_at = EXCLUDED.updated_at`

var frontendURL sql.NullString
if p.FrontendURL != "" {
frontendURL = sql.NullString{String: p.FrontendURL, Valid: true}
}

_, err := s.db.ExecContext(ctx, q,
p.ID, p.Type, p.DisplayName, p.Image, p.Version, p.GRPCAddr,
p.ID, p.Type, p.DisplayName, p.Image, p.Version, p.GRPCAddr, frontendURL,
json.RawMessage(p.Config), json.RawMessage(p.Secrets),
p.Enabled, p.InstalledAt, p.UpdatedAt,
)
Expand Down Expand Up @@ -155,13 +161,14 @@ func (s *PostgresPluginStore) SetSetting(ctx context.Context, key, value string)
func scanPlugin(row *sql.Row) (*plugindomain.Plugin, error) {
var (
p plugindomain.Plugin
frontendURL sql.NullString
config []byte
secrets []byte
installedAt time.Time
updatedAt time.Time
)
err := row.Scan(
&p.ID, &p.Type, &p.DisplayName, &p.Image, &p.Version, &p.GRPCAddr,
&p.ID, &p.Type, &p.DisplayName, &p.Image, &p.Version, &p.GRPCAddr, &frontendURL,
&config, &secrets, &p.Enabled, &installedAt, &updatedAt,
)
if err == sql.ErrNoRows {
Expand All @@ -170,6 +177,7 @@ func scanPlugin(row *sql.Row) (*plugindomain.Plugin, error) {
if err != nil {
return nil, fmt.Errorf("scan plugin: %w", err)
}
p.FrontendURL = frontendURL.String
p.Config = json.RawMessage(config)
p.Secrets = json.RawMessage(secrets)
p.InstalledAt = installedAt.UTC()
Expand All @@ -183,18 +191,20 @@ func scanPlugins(rows *sql.Rows) ([]*plugindomain.Plugin, error) {
for rows.Next() {
var (
p plugindomain.Plugin
frontendURL sql.NullString
config []byte
secrets []byte
installedAt time.Time
updatedAt time.Time
)
err := rows.Scan(
&p.ID, &p.Type, &p.DisplayName, &p.Image, &p.Version, &p.GRPCAddr,
&p.ID, &p.Type, &p.DisplayName, &p.Image, &p.Version, &p.GRPCAddr, &frontendURL,
&config, &secrets, &p.Enabled, &installedAt, &updatedAt,
)
if err != nil {
return nil, fmt.Errorf("scan plugin: %w", err)
}
p.FrontendURL = frontendURL.String
p.Config = json.RawMessage(config)
p.Secrets = json.RawMessage(secrets)
p.InstalledAt = installedAt.UTC()
Expand Down
10 changes: 10 additions & 0 deletions internal/core/plugins/application/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ func (m *Manager) Start(ctx context.Context) error {
m.setStatus(p.ID, domain.PluginStatusDisabled)
continue
}
// Frontend-only plugins have no backend container — mark running immediately.
if p.GRPCAddr == "" {
m.setStatus(p.ID, domain.PluginStatusRunning)
continue
}
if err := m.ensureRunning(ctx, p); err != nil {
m.logger.Warn("plugin manager: startup: failed to start plugin",
"id", p.ID, "error", err)
Expand Down Expand Up @@ -510,6 +515,11 @@ func (m *Manager) runHealthChecks(ctx context.Context) {
}

func (m *Manager) checkPlugin(ctx context.Context, p *domain.Plugin) {
// Frontend-only plugins have no backend container — always healthy.
if p.GRPCAddr == "" {
m.setStatus(p.ID, domain.PluginStatusRunning)
return
}
containerID := "kleff-" + p.ID
st, err := m.rt.Status(ctx, containerID)
if err != nil || st.State != runtime.StateRunning {
Expand Down
4 changes: 4 additions & 0 deletions internal/core/plugins/domain/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ type Plugin struct {
// e.g. "kleff-idp-keycloak:50051"
GRPCAddr string

// FrontendURL is the URL of the plugin's JS bundle served to the browser.
// e.g. "http://localhost:3001/plugin.js". Empty for backend-only plugins.
FrontendURL string

// Config holds non-secret configuration values as a JSON blob.
// Values are env-var key → value, e.g. {"KEYCLOAK_URL": "http://keycloak:8080"}.
Config json.RawMessage
Expand Down
Loading