Skip to content

Commit 271244d

Browse files
committed
feat: add health check interface and status reporting to provider
feat: implement in-memory vault for secrets management feat: add template management functionality to Badger store feat: implement template operations in memory store feat: add template management to MongoDB store feat: implement template operations in PostgreSQL store feat: add template management to SQLite store feat: enhance worker scheduler with runtime status tracking
1 parent db6983d commit 271244d

149 files changed

Lines changed: 39163 additions & 110 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Makefile

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,16 @@ clean c:
9999
## fmt (f): Format code
100100
fmt f:
101101
@echo "$(BLUE)Formatting code...$(NC)"
102-
@gofmt -s -w .
102+
@COUNT=0; \
103+
ROOT_DIR=$$(pwd); \
104+
for modfile in $$(find . -name "go.mod" -type f | grep -v "/vendor/" | sort); do \
105+
dir=$$(dirname $$modfile); \
106+
echo " Formatting $$dir..."; \
107+
cd "$$ROOT_DIR/$$dir" && $(GO) fmt ./...; \
108+
COUNT=$$((COUNT + 1)); \
109+
done; \
110+
cd "$$ROOT_DIR"; \
111+
echo "$(GREEN)✓ Formatted $$COUNT module(s)$(NC)"
103112
@command -v goimports >/dev/null 2>&1 && goimports -w -local github.com/xraph/ctrlplane . || echo "$(YELLOW)goimports not found, skipping (run: go install golang.org/x/tools/cmd/goimports@latest)$(NC)"
104113
@echo "$(GREEN)✓ Formatting complete$(NC)"
105114

admin/admin.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,22 @@ type Service interface {
4040
// ListProviders returns status of all registered providers.
4141
ListProviders(ctx context.Context) ([]ProviderStatus, error)
4242

43+
// TestProviderHealth runs a health check against a specific provider.
44+
TestProviderHealth(ctx context.Context, providerName string) (*ProviderHealthResult, error)
45+
4346
// QueryAuditLog queries the audit log.
4447
QueryAuditLog(ctx context.Context, opts AuditQuery) (*AuditResult, error)
4548
}
4649

50+
// ProviderHealthResult holds the result of a provider health test.
51+
type ProviderHealthResult struct {
52+
Name string `db:"name" json:"name"`
53+
Healthy bool `db:"healthy" json:"healthy"`
54+
Message string `db:"message" json:"message"`
55+
Latency time.Duration `db:"latency" json:"latency"`
56+
CheckedAt time.Time `db:"checked_at" json:"checked_at"`
57+
}
58+
4759
// CreateTenantRequest holds the parameters for creating a tenant.
4860
type CreateTenantRequest struct {
4961
Name string `json:"name" validate:"required"`

admin/service_impl.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,53 @@ func (s *service) ListProviders(ctx context.Context) ([]ProviderStatus, error) {
373373
return statuses, nil
374374
}
375375

376+
// TestProviderHealth runs a health check against a specific provider.
377+
func (s *service) TestProviderHealth(ctx context.Context, providerName string) (*ProviderHealthResult, error) {
378+
claims, err := auth.RequireClaims(ctx)
379+
if err != nil {
380+
return nil, fmt.Errorf("test provider health: %w", err)
381+
}
382+
383+
if !claims.IsSystemAdmin() {
384+
return nil, fmt.Errorf("test provider health: %w", ctrlplane.ErrForbidden)
385+
}
386+
387+
p, err := s.providers.Get(providerName)
388+
if err != nil {
389+
return nil, fmt.Errorf("test provider health: %w", err)
390+
}
391+
392+
// If the provider implements HealthChecker, use it.
393+
if hc, ok := p.(provider.HealthChecker); ok {
394+
status, healthErr := hc.HealthCheck(ctx)
395+
396+
result := &ProviderHealthResult{
397+
Name: providerName,
398+
CheckedAt: time.Now().UTC(),
399+
}
400+
401+
switch {
402+
case healthErr != nil:
403+
result.Message = healthErr.Error()
404+
default:
405+
result.Healthy = status.Healthy
406+
result.Message = status.Message
407+
result.Latency = status.Latency
408+
result.CheckedAt = status.CheckedAt
409+
}
410+
411+
return result, nil
412+
}
413+
414+
// Fall back to reporting healthy if no HealthChecker interface.
415+
return &ProviderHealthResult{
416+
Name: providerName,
417+
Healthy: true,
418+
Message: "provider does not implement health checking",
419+
CheckedAt: time.Now().UTC(),
420+
}, nil
421+
}
422+
376423
// QueryAuditLog queries the audit log.
377424
func (s *service) QueryAuditLog(ctx context.Context, opts AuditQuery) (*AuditResult, error) {
378425
_, err := auth.RequireClaims(ctx)

app/controlplane.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/xraph/ctrlplane/plugin"
1919
"github.com/xraph/ctrlplane/provider"
2020
"github.com/xraph/ctrlplane/secrets"
21+
"github.com/xraph/ctrlplane/secrets/memoryvault"
2122
"github.com/xraph/ctrlplane/store"
2223
"github.com/xraph/ctrlplane/telemetry"
2324
"github.com/xraph/ctrlplane/worker"
@@ -27,6 +28,7 @@ import (
2728
type CtrlPlane struct {
2829
config ctrlplane.Config
2930
store store.Store
31+
vault secrets.Vault
3032
auth auth.Provider
3133
providers *provider.Registry
3234
events event.Bus
@@ -91,6 +93,16 @@ func (cp *CtrlPlane) Events() event.Bus {
9193
return cp.events
9294
}
9395

96+
// Vault returns the vault backend used for secret storage.
97+
func (cp *CtrlPlane) Vault() secrets.Vault {
98+
return cp.vault
99+
}
100+
101+
// Scheduler returns the background worker scheduler.
102+
func (cp *CtrlPlane) Scheduler() *worker.Scheduler {
103+
return cp.scheduler
104+
}
105+
94106
// Extensions returns the plugin registry.
95107
func (cp *CtrlPlane) Extensions() *plugin.Registry {
96108
return cp.extensions
@@ -128,11 +140,16 @@ func (cp *CtrlPlane) Stop(ctx context.Context) error {
128140

129141
// wireServices instantiates all service implementations and background workers.
130142
func (cp *CtrlPlane) wireServices() {
143+
// Default to an in-memory vault when none was provided via WithVault.
144+
if cp.vault == nil {
145+
cp.vault = memoryvault.New()
146+
}
147+
131148
// Instance service.
132149
cp.Instances = instance.NewService(cp.store, cp.providers, cp.events, cp.auth)
133150

134151
// Deploy service with strategies.
135-
deploySvc := deploy.NewService(cp.store, cp.store, cp.providers, cp.events, cp.auth)
152+
deploySvc := deploy.NewService(cp.store, cp.store, cp.providers, cp.events, cp.auth, cp.vault)
136153
deploySvc.RegisterStrategy(strategies.NewRolling())
137154
deploySvc.RegisterStrategy(strategies.NewBlueGreen())
138155
deploySvc.RegisterStrategy(strategies.NewCanary())
@@ -149,8 +166,8 @@ func (cp *CtrlPlane) wireServices() {
149166
// Network service (no external router by default).
150167
cp.Network = network.NewService(cp.store, nil, cp.events, cp.auth)
151168

152-
// Secrets service (no vault by default; users should provide one via options).
153-
cp.Secrets = secrets.NewService(cp.store, nil, cp.auth)
169+
// Secrets service backed by the configured vault.
170+
cp.Secrets = secrets.NewService(cp.store, cp.vault, cp.auth)
154171

155172
// Admin service.
156173
cp.Admin = admin.NewService(cp.store, cp.store, cp.store, cp.store, cp.providers, cp.events, cp.auth)

app/options.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/xraph/ctrlplane/event"
77
"github.com/xraph/ctrlplane/plugin"
88
"github.com/xraph/ctrlplane/provider"
9+
"github.com/xraph/ctrlplane/secrets"
910
"github.com/xraph/ctrlplane/store"
1011
)
1112

@@ -66,6 +67,15 @@ func WithDefaultProvider(name string) Option {
6667
}
6768
}
6869

70+
// WithVault sets the secret storage backend.
71+
func WithVault(v secrets.Vault) Option {
72+
return func(cp *CtrlPlane) error {
73+
cp.vault = v
74+
75+
return nil
76+
}
77+
}
78+
6979
// WithExtension registers a plugin extension with the control plane.
7080
func WithExtension(ext plugin.Extension) Option {
7181
return func(cp *CtrlPlane) error {

cmd/controlplane/main.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55

66
"github.com/xraph/forge"
77

8-
"github.com/xraph/ctrlplane/app"
98
"github.com/xraph/ctrlplane/extension"
109
"github.com/xraph/ctrlplane/provider/docker"
1110
"github.com/xraph/ctrlplane/store/memory"
@@ -34,10 +33,16 @@ func run() error {
3433
})),
3534
)
3635

36+
// Create Docker provider with sane defaults.
37+
dockerProv, err := docker.New()
38+
if err != nil {
39+
return err
40+
}
41+
3742
// Register CtrlPlane as a Forge extension
3843
cpExt := extension.New(
39-
extension.WithStore(app.WithStore(memory.New())),
40-
extension.WithProvider("docker", docker.New(docker.Config{})),
44+
extension.WithStore(memory.New()),
45+
extension.WithProvider("docker", dockerProv),
4146
extension.WithBasePath("/api/cp"),
4247
)
4348

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package components
2+
3+
import (
4+
"github.com/xraph/forgeui/components/badge"
5+
"github.com/xraph/forgeui/components/table"
6+
7+
"github.com/xraph/ctrlplane/admin"
8+
)
9+
10+
// AuditTable renders a table of audit log entries.
11+
templ AuditTable(entries []admin.AuditEntry) {
12+
if len(entries) == 0 {
13+
@EmptyState("scroll-text", "No audit entries", "Actions will be recorded here as they happen.")
14+
} else {
15+
@table.Table() {
16+
@table.Header() {
17+
@table.Row() {
18+
@table.Head() { Time }
19+
@table.Head() { Actor }
20+
@table.Head() { Action }
21+
@table.Head() { Resource }
22+
@table.Head() { Resource ID }
23+
@table.Head() { IP Address }
24+
}
25+
}
26+
@table.Body() {
27+
for _, entry := range entries {
28+
@table.Row() {
29+
@table.Cell() {
30+
<span class="text-muted-foreground text-xs">{ entry.CreatedAt.Format("Jan 02 15:04:05") }</span>
31+
}
32+
@table.Cell() {
33+
<div class="flex flex-col">
34+
<span class="text-sm">{ entry.ActorID }</span>
35+
@badge.Badge(badge.Props{Variant: badge.VariantOutline, Class: "text-[10px] w-fit"}) {
36+
{ entry.ActorType }
37+
}
38+
</div>
39+
}
40+
@table.Cell() {
41+
@badge.Badge(badge.Props{Variant: badge.VariantSecondary}) {
42+
{ entry.Action }
43+
}
44+
}
45+
@table.Cell() {
46+
<span class="text-muted-foreground">{ entry.Resource }</span>
47+
}
48+
@table.Cell() {
49+
<span class="font-mono text-xs text-muted-foreground">{ truncStr(entry.ResourceID, 24) }</span>
50+
}
51+
@table.Cell() {
52+
<span class="text-muted-foreground text-xs">{ entry.IPAddress }</span>
53+
}
54+
}
55+
}
56+
}
57+
}
58+
}
59+
}

0 commit comments

Comments
 (0)