This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
// Package servicedefaults installs the small, explicitly configured service
|
||||
// monitoring baseline used by a production Pulse deployment.
|
||||
package servicedefaults
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/itworx/pulse/internal/probe"
|
||||
"github.com/itworx/pulse/internal/reconciliation"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
const identityNamespace = "itworx-pulse-service-defaults"
|
||||
|
||||
type Options struct {
|
||||
PublicURL string
|
||||
OIDCIssuer string
|
||||
}
|
||||
|
||||
type Summary struct {
|
||||
Services int
|
||||
Endpoints int
|
||||
Probes int
|
||||
Dependencies int
|
||||
}
|
||||
|
||||
type serviceSeed struct {
|
||||
id, name, description string
|
||||
endpointID string
|
||||
endpointName string
|
||||
endpointType string
|
||||
target probe.Target
|
||||
probes []probeSeed
|
||||
}
|
||||
|
||||
type probeSeed struct {
|
||||
id, name, kind string
|
||||
target probe.Target
|
||||
interval int
|
||||
timeout int
|
||||
expected []int
|
||||
verifyTLS bool
|
||||
}
|
||||
|
||||
// Seed validates every configured URL against the same network policy used at
|
||||
// execution time, then atomically installs or refreshes only Pulse-owned rows.
|
||||
func Seed(ctx context.Context, pool *pgxpool.Pool, options Options, policy probe.NetworkPolicy, resolver probe.Resolver) (Summary, error) {
|
||||
if pool == nil {
|
||||
return Summary{}, errors.New("service defaults require a database pool")
|
||||
}
|
||||
seeds, err := desired(ctx, options, policy, resolver)
|
||||
if err != nil {
|
||||
return Summary{}, err
|
||||
}
|
||||
if len(seeds) == 0 {
|
||||
return Summary{}, nil
|
||||
}
|
||||
tx, err := pool.BeginTx(ctx, pgx.TxOptions{})
|
||||
if err != nil {
|
||||
return Summary{}, fmt.Errorf("begin service defaults: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
|
||||
summary := Summary{}
|
||||
for _, service := range seeds {
|
||||
labels := []byte(`{"managedBy":"pulse-system-defaults"}`)
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO services (id,name,description,state,labels,revision)
|
||||
VALUES ($1,$2,$3,'unknown',$4::jsonb,1)
|
||||
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name,description=EXCLUDED.description,
|
||||
labels=EXCLUDED.labels,archived_at=NULL,updated_at=now(),revision=services.revision+1
|
||||
WHERE (services.name,services.description,services.labels,services.archived_at) IS DISTINCT FROM
|
||||
(EXCLUDED.name,EXCLUDED.description,EXCLUDED.labels,EXCLUDED.archived_at)`,
|
||||
service.id, service.name, service.description, labels); err != nil {
|
||||
return Summary{}, fmt.Errorf("upsert system service %q: %w", service.name, err)
|
||||
}
|
||||
summary.Services++
|
||||
target, err := json.Marshal(service.target)
|
||||
if err != nil {
|
||||
return Summary{}, fmt.Errorf("encode endpoint target: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO service_endpoints (id,service_id,name,endpoint_type,target,enabled,revision)
|
||||
VALUES ($1,$2,$3,$4,$5::jsonb,true,1)
|
||||
ON CONFLICT (id) DO UPDATE SET name=EXCLUDED.name,endpoint_type=EXCLUDED.endpoint_type,
|
||||
target=EXCLUDED.target,enabled=true,archived_at=NULL,updated_at=now(),revision=service_endpoints.revision+1
|
||||
WHERE (service_endpoints.name,service_endpoints.endpoint_type,service_endpoints.target,service_endpoints.enabled,service_endpoints.archived_at) IS DISTINCT FROM
|
||||
(EXCLUDED.name,EXCLUDED.endpoint_type,EXCLUDED.target,EXCLUDED.enabled,EXCLUDED.archived_at)`,
|
||||
service.endpointID, service.id, service.endpointName, service.endpointType, target); err != nil {
|
||||
return Summary{}, fmt.Errorf("upsert system endpoint %q: %w", service.endpointName, err)
|
||||
}
|
||||
summary.Endpoints++
|
||||
for _, definition := range service.probes {
|
||||
target, err := json.Marshal(definition.target)
|
||||
if err != nil {
|
||||
return Summary{}, fmt.Errorf("encode probe target: %w", err)
|
||||
}
|
||||
expected, err := json.Marshal(definition.expected)
|
||||
if err != nil {
|
||||
return Summary{}, fmt.Errorf("encode expected statuses: %w", err)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO probes
|
||||
(id,service_id,endpoint_id,name,probe_type,target,interval_seconds,timeout_seconds,enabled,expected_status_codes,follow_redirects,verify_tls,revision)
|
||||
VALUES ($1,$2,$3,$4,$5,$6::jsonb,$7,$8,true,$9::jsonb,false,$10,1)
|
||||
ON CONFLICT (id) DO UPDATE SET endpoint_id=EXCLUDED.endpoint_id,name=EXCLUDED.name,
|
||||
probe_type=EXCLUDED.probe_type,target=EXCLUDED.target,interval_seconds=EXCLUDED.interval_seconds,
|
||||
timeout_seconds=EXCLUDED.timeout_seconds,enabled=true,expected_status_codes=EXCLUDED.expected_status_codes,
|
||||
follow_redirects=false,verify_tls=EXCLUDED.verify_tls,archived_at=NULL,updated_at=now(),revision=probes.revision+1
|
||||
WHERE (probes.endpoint_id,probes.name,probes.probe_type,probes.target,probes.interval_seconds,probes.timeout_seconds,
|
||||
probes.enabled,probes.expected_status_codes,probes.follow_redirects,probes.verify_tls,probes.archived_at) IS DISTINCT FROM
|
||||
(EXCLUDED.endpoint_id,EXCLUDED.name,EXCLUDED.probe_type,EXCLUDED.target,EXCLUDED.interval_seconds,EXCLUDED.timeout_seconds,
|
||||
EXCLUDED.enabled,EXCLUDED.expected_status_codes,EXCLUDED.follow_redirects,EXCLUDED.verify_tls,EXCLUDED.archived_at)`,
|
||||
definition.id, service.id, service.endpointID, definition.name, definition.kind, target,
|
||||
definition.interval, definition.timeout, expected, definition.verifyTLS); err != nil {
|
||||
return Summary{}, fmt.Errorf("upsert system probe %q: %w", definition.name, err)
|
||||
}
|
||||
summary.Probes++
|
||||
}
|
||||
}
|
||||
if len(seeds) == 2 {
|
||||
dependencyID := stableID("dependency:pulse-authentik")
|
||||
if _, err := tx.Exec(ctx, `INSERT INTO service_dependencies
|
||||
(id,service_id,depends_on_service_id,relation_type,confidence,confirmed,first_seen_at,last_seen_at)
|
||||
VALUES ($1,$2,$3,'depends_on',1,true,now(),now())
|
||||
ON CONFLICT (service_id,depends_on_service_id,relation_type) WHERE source_id IS NULL
|
||||
DO UPDATE SET confidence=1,confirmed=true,last_seen_at=now(),archived_at=NULL`,
|
||||
dependencyID, seeds[0].id, seeds[1].id); err != nil {
|
||||
return Summary{}, fmt.Errorf("upsert Pulse authentication dependency: %w", err)
|
||||
}
|
||||
summary.Dependencies = 1
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return Summary{}, fmt.Errorf("commit service defaults: %w", err)
|
||||
}
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
func desired(ctx context.Context, options Options, policy probe.NetworkPolicy, resolver probe.Resolver) ([]serviceSeed, error) {
|
||||
publicURL := strings.TrimSpace(options.PublicURL)
|
||||
issuerURL := strings.TrimSpace(options.OIDCIssuer)
|
||||
if publicURL == "" && issuerURL == "" {
|
||||
return nil, nil
|
||||
}
|
||||
if publicURL == "" || issuerURL == "" {
|
||||
return nil, errors.New("PULSE_PUBLIC_URL and PULSE_OIDC_ISSUER must both be configured for system service monitoring")
|
||||
}
|
||||
public, err := validateHTTPS(ctx, policy, resolver, publicURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validate PULSE_PUBLIC_URL for service monitoring: %w", err)
|
||||
}
|
||||
issuer, err := validateHTTPS(ctx, policy, resolver, issuerURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validate PULSE_OIDC_ISSUER for service monitoring: %w", err)
|
||||
}
|
||||
publicPort := urlPort(public)
|
||||
issuerPort := urlPort(issuer)
|
||||
healthTarget := probe.Target{Scheme: "https", Host: public.Hostname(), Port: publicPort, Path: joinURLPath(public.Path, "healthz")}
|
||||
issuerTarget := probe.Target{Scheme: "https", Host: issuer.Hostname(), Port: issuerPort, Path: joinURLPath(issuer.Path, ".well-known/openid-configuration")}
|
||||
tlsTarget := probe.Target{Scheme: "tls", Host: public.Hostname(), Port: publicPort}
|
||||
return []serviceSeed{
|
||||
{id: stableID("service:pulse"), name: "ITWorx Pulse", description: "Publieke Pulse-applicatie en API", endpointID: stableID("endpoint:pulse-public"), endpointName: "Publieke HTTPS-endpoint", endpointType: "http", target: healthTarget,
|
||||
probes: []probeSeed{{id: stableID("probe:pulse-health"), name: "Pulse health", kind: probe.TypeHTTP, target: healthTarget, interval: 30, timeout: 10, expected: []int{200}, verifyTLS: true}, {id: stableID("probe:pulse-tls"), name: "Pulse TLS-certificaat", kind: probe.TypeTLS, target: tlsTarget, interval: 21600, timeout: 10, verifyTLS: true}}},
|
||||
{id: stableID("service:authentik"), name: "Authentik SSO", description: "OIDC-identiteitsprovider voor Pulse", endpointID: stableID("endpoint:authentik-discovery"), endpointName: "OIDC discovery-endpoint", endpointType: "http", target: issuerTarget,
|
||||
probes: []probeSeed{{id: stableID("probe:authentik-discovery"), name: "Authentik OIDC discovery", kind: probe.TypeHTTP, target: issuerTarget, interval: 60, timeout: 10, expected: []int{200}, verifyTLS: true}}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateHTTPS(ctx context.Context, policy probe.NetworkPolicy, resolver probe.Resolver, raw string) (*url.URL, error) {
|
||||
parsed, err := policy.ValidateURL(ctx, resolver, raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsed.Scheme != "https" || parsed.RawQuery != "" || parsed.User != nil || parsed.Fragment != "" {
|
||||
return nil, errors.New("system service URL must be HTTPS without credentials, query parameters, or fragment")
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func joinURLPath(base, suffix string) string {
|
||||
joined := path.Join("/", base, suffix)
|
||||
if !strings.HasPrefix(joined, "/") {
|
||||
return "/" + joined
|
||||
}
|
||||
return joined
|
||||
}
|
||||
|
||||
func urlPort(value *url.URL) int {
|
||||
if value.Port() == "" {
|
||||
return 443
|
||||
}
|
||||
var port int
|
||||
_, _ = fmt.Sscanf(value.Port(), "%d", &port)
|
||||
return port
|
||||
}
|
||||
|
||||
func stableID(key string) string {
|
||||
return reconciliation.StableEntityID(identityNamespace, "service-default", key)
|
||||
}
|
||||
Reference in New Issue
Block a user