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
9 changes: 9 additions & 0 deletions internal/bootstrap/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ type Config struct {
// JWKSUri is the OIDC JWKS endpoint for JWT signature verification.
JWKSUri string

// ── Crate registry ────────────────────────────────────────────────────────

// CrateRegistryURL is the base URL of the crate registry.
// Default: https://raw.githubusercontent.com/kleffio/crate-registry/main
// For local dev, use: file:///absolute/path/to/crate-registry
CrateRegistryURL string

// ── Plugin system ─────────────────────────────────────────────────────────

// RuntimeProvider selects the container runtime for plugin management.
Expand Down Expand Up @@ -86,6 +93,8 @@ func LoadConfig() (*Config, error) {
IntrospectClientSecret: config.String("INTROSPECT_CLIENT_SECRET", ""),
JWKSUri: config.String("JWKS_URI", ""),

CrateRegistryURL: config.String("CRATE_REGISTRY_URL", ""),

RuntimeProvider: config.String("RUNTIME_PROVIDER", "docker"),
PluginRegistryURL: config.String("PLUGIN_REGISTRY_URL", ""),
PluginNetwork: config.String("PLUGIN_NETWORK", "kleff"),
Expand Down
16 changes: 16 additions & 0 deletions internal/bootstrap/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import (
adminhttp "github.com/kleffio/platform/internal/core/admin/adapters/http"
audithttp "github.com/kleffio/platform/internal/core/audit/adapters/http"
billinghttp "github.com/kleffio/platform/internal/core/billing/adapters/http"
cataloghttp "github.com/kleffio/platform/internal/core/catalog/adapters/http"
catalogpersistence "github.com/kleffio/platform/internal/core/catalog/adapters/persistence"
catalogregistry "github.com/kleffio/platform/internal/core/catalog/adapters/registry"
deploymentshttp "github.com/kleffio/platform/internal/core/deployments/adapters/http"
nodeshttp "github.com/kleffio/platform/internal/core/nodes/adapters/http"
organizationshttp "github.com/kleffio/platform/internal/core/organizations/adapters/http"
Expand Down Expand Up @@ -43,6 +46,7 @@ type Container struct {
// HTTP handler groups per domain module
AuthHandler *pluginhttp.AuthHandler
SetupHandler *pluginhttp.SetupHandler
CatalogHandler *cataloghttp.Handler
OrganizationsHandler *organizationshttp.Handler
DeploymentsHandler *deploymentshttp.Handler
NodesHandler *nodeshttp.Handler
Expand Down Expand Up @@ -84,6 +88,17 @@ func NewContainer(cfg *Config, logger *slog.Logger) (*Container, error) {
// Non-fatal: server continues even if some plugins fail to start.
}

catalogStore := catalogpersistence.NewPostgresCatalogStore(db)

// Sync crates, blueprints, and constructs from the remote crate registry.
// Non-fatal: if the registry is unreachable on startup, existing DB data is used.
crateRegistry := catalogregistry.New(cfg.CrateRegistryURL)
if err := crateRegistry.Sync(context.Background(), catalogStore); err != nil {
logger.Warn("crate registry sync warning", "error", err)
} else {
logger.Info("crate registry synced")
}

return &Container{
Config: cfg,
Logger: logger,
Expand All @@ -93,6 +108,7 @@ func NewContainer(cfg *Config, logger *slog.Logger) (*Container, error) {

AuthHandler: pluginhttp.NewAuthHandler(pluginMgr, logger),
SetupHandler: pluginhttp.NewSetupHandler(pluginMgr, catalogRegistry, logger),
CatalogHandler: cataloghttp.NewHandler(catalogStore, logger),
OrganizationsHandler: organizationshttp.NewHandler(logger),
DeploymentsHandler: deploymentshttp.NewHandler(logger),
NodesHandler: nodeshttp.NewHandler(logger),
Expand Down
3 changes: 3 additions & 0 deletions internal/bootstrap/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ func buildRouter(c *Container) http.Handler {
// Public setup routes — only active before the first IDP is installed.
c.SetupHandler.RegisterPublicRoutes(r)

// Catalog (crates + blueprints) is public — no login needed to browse.
c.CatalogHandler.RegisterRoutes(r)

// Authenticated routes
r.Group(func(r chi.Router) {
r.Use(middleware.RequireAuth(c.TokenVerifier))
Expand Down
119 changes: 119 additions & 0 deletions internal/core/catalog/adapters/http/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package http

import (
"encoding/json"
"log/slog"
"net/http"

"github.com/go-chi/chi/v5"
"github.com/kleffio/platform/internal/core/catalog/ports"
)

// Handler exposes the catalog (crates, blueprints, constructs) over HTTP.
type Handler struct {
repo ports.CatalogRepository
logger *slog.Logger
}

func NewHandler(repo ports.CatalogRepository, logger *slog.Logger) *Handler {
return &Handler{repo: repo, logger: logger}
}

func (h *Handler) RegisterRoutes(r chi.Router) {
r.Get("/api/v1/crates", h.listCrates)
r.Get("/api/v1/crates/{id}", h.getCrate)
r.Get("/api/v1/blueprints", h.listBlueprints)
r.Get("/api/v1/blueprints/{id}", h.getBlueprint)
r.Get("/api/v1/constructs", h.listConstructs)
r.Get("/api/v1/constructs/{id}", h.getConstruct)
}

func (h *Handler) listCrates(w http.ResponseWriter, r *http.Request) {
category := r.URL.Query().Get("category")

crates, err := h.repo.ListCrates(r.Context(), category)
if err != nil {
h.internalError(w, "list crates", err)
return
}

writeJSON(w, http.StatusOK, map[string]any{"crates": crates})
}

func (h *Handler) getCrate(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")

crate, err := h.repo.GetCrate(r.Context(), id)
if err != nil {
h.notFound(w, err)
return
}

writeJSON(w, http.StatusOK, crate)
}

func (h *Handler) listBlueprints(w http.ResponseWriter, r *http.Request) {
crateID := r.URL.Query().Get("crate")

blueprints, err := h.repo.ListBlueprints(r.Context(), crateID)
if err != nil {
h.internalError(w, "list blueprints", err)
return
}

writeJSON(w, http.StatusOK, map[string]any{"blueprints": blueprints})
}

func (h *Handler) getBlueprint(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")

blueprint, err := h.repo.GetBlueprint(r.Context(), id)
if err != nil {
h.notFound(w, err)
return
}

writeJSON(w, http.StatusOK, blueprint)
}

func (h *Handler) listConstructs(w http.ResponseWriter, r *http.Request) {
crateID := r.URL.Query().Get("crate")
blueprintID := r.URL.Query().Get("blueprint")

constructs, err := h.repo.ListConstructs(r.Context(), crateID, blueprintID)
if err != nil {
h.internalError(w, "list constructs", err)
return
}

writeJSON(w, http.StatusOK, map[string]any{"constructs": constructs})
}

func (h *Handler) getConstruct(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")

construct, err := h.repo.GetConstruct(r.Context(), id)
if err != nil {
h.notFound(w, err)
return
}

writeJSON(w, http.StatusOK, construct)
}

// ── helpers ───────────────────────────────────────────────────────────────────

func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}

func (h *Handler) internalError(w http.ResponseWriter, op string, err error) {
h.logger.Error(op, "error", err)
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"})
}

func (h *Handler) notFound(w http.ResponseWriter, err error) {
writeJSON(w, http.StatusNotFound, map[string]string{"error": err.Error()})
}
Loading
Loading