Skip to content
4 changes: 2 additions & 2 deletions persys-automation/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ func Load() (*Config, error) {
),
VaultPKIMount: envOr("AUTOMATION_VAULT_PKI_MOUNT", "pki"),
VaultPKIRole: envOr("AUTOMATION_VAULT_PKI_ROLE", "persys-automation"),
VaultCertTTL: envDurationOr("AUTOMATION_VAULT_CERT_TTL", 24*time.Hour),
VaultCertTTL: envDurationOr("AUTOMATION_VAULT_CERT_TTL", 1*time.Hour),
VaultServiceName: envOr("AUTOMATION_VAULT_SERVICE_NAME", "persys-automation"),
VaultServiceDomain: strings.TrimSpace(os.Getenv("AUTOMATION_VAULT_SERVICE_DOMAIN")),
VaultRetryInterval: envDurationOr("AUTOMATION_VAULT_RETRY_INTERVAL", time.Minute),
VaultRetryInterval: envDurationOr("AUTOMATION_VAULT_RETRY_INTERVAL", 30*time.Second),
ForgeryRedisEnabled: envBoolOr(
"AUTOMATION_FORGERY_REDIS_ENABLED",
false,
Expand Down
2 changes: 1 addition & 1 deletion persys-forgery/utils/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func (c *Config) applyDefaults() {
c.Vault.AuthMethod = "token"
}
if c.Vault.CertTTL == 0 {
c.Vault.CertTTL = 24 * time.Hour
c.Vault.CertTTL = 1 * time.Hour
}
if c.Vault.RetryInterval == 0 {
c.Vault.RetryInterval = 30 * time.Second
Expand Down
1 change: 1 addition & 0 deletions persys-gateway/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ WORKDIR /app

COPY --from=build /src/persys-gateway/config.yaml .
COPY --from=build /src/persys-gateway/cluster.yaml .
COPY --from=build /src/persys-gateway/catalog.yaml .
COPY --from=build /src/persys-gateway/cmd/persys-gateway .

RUN chmod +x ./persys-gateway
Expand Down
8 changes: 8 additions & 0 deletions persys-gateway/catalog.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
services:
- name: meter
path_prefix: /meter
upstream_addr: http://persys-meter:9092
strip_prefix: true
auth: none
timeout: 10s
enabled: true
5 changes: 5 additions & 0 deletions persys-gateway/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type App struct {
authController controllers.AuthController
githubController controllers.GithubController
clusterMetaController *controllers.ClusterMetaController
eventsController *controllers.EventsController
webhookController *controllers.WebhookController
automationController *controllers.AutomationController
}
Expand Down Expand Up @@ -190,6 +191,7 @@ func main() {
)
app.githubController = controllers.NewGithubController(app.authService, ctx, app.githubService, cnf)
app.clusterMetaController = controllers.NewClusterMetaController(app.clusterControl, string(cnf.Deployment.Mode), cnf.Database.Enabled())
app.eventsController = controllers.NewEventsController(app.clusterControl)
app.webhookController = controllers.NewWebhookController(app.webhookService)
app.automationController = controllers.NewAutomationController(app.automationService)

Expand Down Expand Up @@ -229,6 +231,9 @@ func main() {
authnMW := authn.New(jwtSecret)
gwRouter := router.New(authnMW, cnf.Deployment.Mode)

// events/watch is registered on the mTLS router group
app.eventsController.Register(mtlsGroup)

// ClusterMetaController: health/list-clusters/get-cluster — the only
// handlers left that were never RPC-shaped.
gwRouter.RegisterControllers(mtlsGroup, app.clusterMetaController)
Expand Down
2 changes: 1 addition & 1 deletion persys-gateway/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ func (c *Config) applyDefaults() {
c.Vault.ManagerAddr = "vault-manager:50069"
}
if c.Vault.CertTTL == time.Duration(0) {
c.Vault.CertTTL = 24 * time.Hour
c.Vault.CertTTL = 1 * time.Hour
}
if c.Vault.RetryInterval == time.Duration(0) {
c.Vault.RetryInterval = 30 * time.Second
Expand Down
90 changes: 90 additions & 0 deletions persys-gateway/controllers/events.controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package controllers

import (
"context"
"io"
"strings"

"github.com/gin-gonic/gin"
controlv1 "github.com/persys-dev/persys-cloud/persys-gateway/internal/controlv1"
"github.com/persys-dev/persys-cloud/persys-gateway/services"
)

// EventsController exposes cluster-wide scheduler events to browser-based
// dashboard clients over Server-Sent Events. WatchEvents is a
// server-streaming gRPC RPC, which grpcbridge explicitly does not
// auto-bridge (see internal/grpcbridge/bridge.go) — this hand-written
// endpoint is the browser-facing equivalent. A gRPC-native client (e.g.
// persysctl) should call the scheduler's WatchEvents RPC directly instead
// of going through this endpoint.
type EventsController struct {
clusterControl *services.ClusterControlService
}

func NewEventsController(clusterControl *services.ClusterControlService) *EventsController {
return &EventsController{clusterControl: clusterControl}
}

// Register mounts /events/watch on rg, — this
// is dashboard-user-facing mTLS-gated SSE endpoint for cluster-wide scheduler events. A gRPC-native client.
func (c *EventsController) Register(rg *gin.RouterGroup) {
rg.GET("/events/watch", c.WatchHandler())
}

// WatchHandler streams cluster-wide scheduler events to the client as
// Server-Sent Events, replaying recent history first (same semantics as
// the underlying WatchEvents RPC). Supports the same optional filters as
// query params: ?type=&workload_id=&node_id=&cluster_id=
func (c *EventsController) WatchHandler() gin.HandlerFunc {
return func(ctx *gin.Context) {
clusterID := strings.TrimSpace(ctx.Query("cluster_id"))
req := &controlv1.WatchEventsRequest{
Type: strings.TrimSpace(ctx.Query("type")),
WorkloadId: strings.TrimSpace(ctx.Query("workload_id")),
NodeId: strings.TrimSpace(ctx.Query("node_id")),
}

ctx.Header("Content-Type", "text/event-stream")
ctx.Header("Cache-Control", "no-cache")
ctx.Header("Connection", "keep-alive")
// Disable response buffering if this gateway ever sits behind
// nginx — otherwise SSE events sit in a buffer instead of
// reaching the browser as they arrive.
ctx.Header("X-Accel-Buffering", "no")

streamCtx, cancel := context.WithCancel(ctx.Request.Context())
defer cancel()

events := make(chan *controlv1.SchedulerEventView, 16)
go func() {
defer close(events)
_ = c.clusterControl.WatchEventsForClient(streamCtx, clusterID, req, func(event *controlv1.SchedulerEventView) error {
select {
case events <- event:
return nil
case <-streamCtx.Done():
return streamCtx.Err()
}
})
// Errors here (scheduler unreachable, stream ended, etc) are
// intentionally swallowed rather than surfaced to the HTTP
// response: by the time an error could occur, headers are
// already flushed and the client is mid-stream — the
// connection simply closes, and a well-behaved EventSource
// client on the dashboard side reconnects on its own.
}()

ctx.Stream(func(w io.Writer) bool {
select {
case event, ok := <-events:
if !ok {
return false
}
ctx.SSEvent("event", event)
return true
case <-streamCtx.Done():
return false
}
})
}
}
Loading
Loading