mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
fix: operator changes
This commit is contained in:
@@ -601,6 +601,10 @@ func (r *InfisicalSecretReconciler) OpenInstantUpdatesStream(ctx context.Context
|
||||
Event: "secret:delete",
|
||||
Conditions: conditions,
|
||||
},
|
||||
{
|
||||
Event: "secret:import-mutation",
|
||||
Conditions: conditions,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -612,9 +616,10 @@ func (r *InfisicalSecretReconciler) OpenInstantUpdatesStream(ctx context.Context
|
||||
headers := map[string]string{
|
||||
"User-Agent": api.USER_AGENT_NAME,
|
||||
"Authorization": fmt.Sprint("Bearer ", token),
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/v1/events/subscribe/project-events", api.API_HOST_URL), strings.NewReader(string(body)))
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/v1/events/subscribe/project-events", api.API_HOST_URL), strings.NewReader(string(body)))
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -2,48 +2,61 @@ package sse
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SSEEvent struct {
|
||||
// Event represents a Server-Sent Event
|
||||
type Event struct {
|
||||
ID string
|
||||
Event string
|
||||
Data string
|
||||
Retry int
|
||||
}
|
||||
|
||||
// SSEClient handles SSE connections
|
||||
type SSEClient struct {
|
||||
URL string
|
||||
Client *http.Client
|
||||
LastHealthCheck time.Time
|
||||
mu *sync.Mutex // for safe concurrent access to LastHealthCheck
|
||||
// Client handles SSE connections with high performance
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
onPing func() // Callback for ping events
|
||||
}
|
||||
|
||||
// NewClient creates a new SSE client
|
||||
func NewClient() SSEClient {
|
||||
return SSEClient{
|
||||
mu: &sync.Mutex{},
|
||||
Client: &http.Client{
|
||||
// NewClient creates a new high-performance SSE client
|
||||
func NewClient() Client {
|
||||
return Client{
|
||||
httpClient: &http.Client{
|
||||
Timeout: 0, // No timeout for streaming
|
||||
Transport: &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
DisableCompression: true, // SSE typically doesn't benefit from compression
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes SSE connection and returns a channel of events
|
||||
func (c *SSEClient) Connect(req *http.Request) (<-chan SSEEvent, <-chan error, error) {
|
||||
// Set required headers for SSE
|
||||
req.Header.Set("Cache-Control", "no-cache")
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
// WithPingHandler sets a callback for ping events
|
||||
func (c *Client) WithPingHandler(handler func()) *Client {
|
||||
c.onPing = handler
|
||||
return c
|
||||
}
|
||||
|
||||
resp, err := c.Client.Do(req)
|
||||
// Connect establishes an SSE connection and returns event channels
|
||||
func (c *Client) Connect(ctx context.Context, req *http.Request) (<-chan Event, <-chan error, error) {
|
||||
// Configure SSE headers
|
||||
req.Header.Set("Cache-Control", "no-cache")
|
||||
req.Header.Set("Accept", "text/event-stream")
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
|
||||
// Add context to request
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -51,66 +64,124 @@ func (c *SSEClient) Connect(req *http.Request) (<-chan SSEEvent, <-chan error, e
|
||||
return nil, nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
eventChan := make(chan SSEEvent)
|
||||
errorChan := make(chan error)
|
||||
eventChan := make(chan Event, 10)
|
||||
errorChan := make(chan error, 1)
|
||||
|
||||
go c.stream(resp.Body, eventChan, errorChan)
|
||||
go c.stream(ctx, resp.Body, eventChan, errorChan)
|
||||
|
||||
return eventChan, errorChan, nil
|
||||
}
|
||||
|
||||
func (c *SSEClient) stream(body io.ReadCloser, eventChan chan<- SSEEvent, errorChan chan<- error) {
|
||||
func (c *Client) stream(ctx context.Context, body io.ReadCloser, eventChan chan<- Event, errorChan chan<- error) {
|
||||
defer body.Close()
|
||||
defer close(eventChan)
|
||||
defer close(errorChan)
|
||||
|
||||
scanner := bufio.NewScanner(body)
|
||||
var event SSEEvent
|
||||
|
||||
var currentEvent Event
|
||||
var dataBuilder strings.Builder
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// End of event
|
||||
if line == "" {
|
||||
if event.Data != "" || event.Event != "" {
|
||||
if strings.TrimSpace(event.Data) == "1" {
|
||||
c.mu.Lock()
|
||||
c.LastHealthCheck = time.Now()
|
||||
c.mu.Unlock()
|
||||
} else if event.Event != "ping" {
|
||||
eventChan <- event
|
||||
line := scanner.Text()
|
||||
|
||||
// Empty line indicates end of event
|
||||
if len(line) == 0 {
|
||||
if currentEvent.Data != "" || currentEvent.Event != "" {
|
||||
// Finalize data
|
||||
if dataBuilder.Len() > 0 {
|
||||
currentEvent.Data = dataBuilder.String()
|
||||
dataBuilder.Reset()
|
||||
}
|
||||
|
||||
event = SSEEvent{} // Reset for next event
|
||||
// Handle ping events
|
||||
if c.isPingEvent(currentEvent) {
|
||||
if c.onPing != nil {
|
||||
c.onPing()
|
||||
}
|
||||
} else {
|
||||
// Send non-ping events
|
||||
select {
|
||||
case eventChan <- currentEvent:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Reset for next event
|
||||
currentEvent = Event{}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(line, "data:"):
|
||||
data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
||||
if event.Data != "" {
|
||||
event.Data += "\n"
|
||||
}
|
||||
event.Data += data
|
||||
|
||||
case strings.HasPrefix(line, "event:"):
|
||||
event.Event = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
|
||||
|
||||
case strings.HasPrefix(line, "id:"):
|
||||
event.ID = strings.TrimSpace(strings.TrimPrefix(line, "id:"))
|
||||
|
||||
case strings.HasPrefix(line, "retry:"):
|
||||
// Optional: parse and apply retry interval here
|
||||
|
||||
case strings.HasPrefix(line, ":"):
|
||||
// Comment line — ignored
|
||||
default:
|
||||
// Unknown line format — can log/debug if needed
|
||||
}
|
||||
// Parse line efficiently
|
||||
c.parseLine(line, ¤tEvent, &dataBuilder)
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
errorChan <- err
|
||||
select {
|
||||
case errorChan <- err:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseLine efficiently parses SSE protocol lines
|
||||
func (c *Client) parseLine(line string, event *Event, dataBuilder *strings.Builder) {
|
||||
colonIndex := strings.IndexByte(line, ':')
|
||||
if colonIndex == -1 {
|
||||
return // Invalid line format
|
||||
}
|
||||
|
||||
field := line[:colonIndex]
|
||||
value := line[colonIndex+1:]
|
||||
|
||||
// Trim leading space from value (SSE spec)
|
||||
if len(value) > 0 && value[0] == ' ' {
|
||||
value = value[1:]
|
||||
}
|
||||
|
||||
switch field {
|
||||
case "data":
|
||||
if dataBuilder.Len() > 0 {
|
||||
dataBuilder.WriteByte('\n')
|
||||
}
|
||||
dataBuilder.WriteString(value)
|
||||
case "event":
|
||||
event.Event = value
|
||||
case "id":
|
||||
event.ID = value
|
||||
case "retry":
|
||||
// Parse retry value if needed
|
||||
// This could be used to configure reconnection delay
|
||||
case "":
|
||||
// Comment line, ignore
|
||||
}
|
||||
}
|
||||
|
||||
// isPingEvent checks if an event is a ping/keepalive
|
||||
func (c *Client) isPingEvent(event Event) bool {
|
||||
// Check for common ping patterns
|
||||
if event.Event == "ping" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for heartbeat data (common pattern is "1" or similar)
|
||||
if event.Event == "" && strings.TrimSpace(event.Data) == "1" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// WithHTTPClient sets a custom HTTP client
|
||||
func (c *Client) WithHTTPClient(client *http.Client) *Client {
|
||||
c.httpClient = client
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -5,161 +5,203 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ConnectionMeta holds metadata about an SSE connection
|
||||
type ConnectionMeta struct {
|
||||
EventChan <-chan SSEEvent
|
||||
EventChan <-chan Event
|
||||
ErrorChan <-chan error
|
||||
LastPingAt time.Time
|
||||
Cancel context.CancelFunc
|
||||
lastPingAt atomic.Value // stores time.Time
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type ConnectionRegistry struct {
|
||||
Ctx context.Context
|
||||
meta *ConnectionMeta
|
||||
client SSEClient
|
||||
mu sync.RWMutex
|
||||
|
||||
monitorCancel context.CancelFunc
|
||||
monitorCtx context.Context
|
||||
// LastPing returns the last ping time
|
||||
func (c *ConnectionMeta) LastPing() time.Time {
|
||||
if t, ok := c.lastPingAt.Load().(time.Time); ok {
|
||||
return t
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func NewConnectionRegistry(ctx context.Context) *ConnectionRegistry {
|
||||
monitorCtx, monitorCancel := context.WithCancel(ctx)
|
||||
return &ConnectionRegistry{
|
||||
Ctx: ctx,
|
||||
client: NewClient(),
|
||||
monitorCtx: monitorCtx,
|
||||
monitorCancel: monitorCancel,
|
||||
// UpdateLastPing atomically updates the last ping time
|
||||
func (c *ConnectionMeta) UpdateLastPing() {
|
||||
c.lastPingAt.Store(time.Now())
|
||||
}
|
||||
|
||||
// Cancel terminates the connection
|
||||
func (c *ConnectionMeta) Cancel() {
|
||||
if c.cancel != nil {
|
||||
c.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// create creates a new connection
|
||||
func (r *ConnectionRegistry) create(req *http.Request) (*ConnectionMeta, error) {
|
||||
// Create new connection using provided request
|
||||
eventChan, errorChan, err := r.client.Connect(req)
|
||||
// ConnectionRegistry manages SSE connections with high performance
|
||||
type ConnectionRegistry struct {
|
||||
ctx context.Context
|
||||
client Client
|
||||
|
||||
mu sync.RWMutex
|
||||
conn *ConnectionMeta
|
||||
|
||||
monitorOnce sync.Once
|
||||
monitorStop chan struct{}
|
||||
}
|
||||
|
||||
// NewConnectionRegistry creates a new high-performance connection registry
|
||||
func NewConnectionRegistry(ctx context.Context) *ConnectionRegistry {
|
||||
r := &ConnectionRegistry{
|
||||
ctx: ctx,
|
||||
monitorStop: make(chan struct{}),
|
||||
}
|
||||
|
||||
// Configure client with ping handler
|
||||
r.client = NewClient()
|
||||
r.client.WithPingHandler(func() {
|
||||
r.UpdateLastPing()
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Subscribe provides SSE events, creating a connection if needed
|
||||
func (r *ConnectionRegistry) Subscribe(buildRequest func() (*http.Request, error)) (<-chan Event, <-chan error, error) {
|
||||
// Fast path: check if connection exists
|
||||
if conn := r.getConnection(); conn != nil {
|
||||
return conn.EventChan, conn.ErrorChan, nil
|
||||
}
|
||||
|
||||
// Slow path: create new connection under lock
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
// Double-check after acquiring lock
|
||||
if r.conn != nil {
|
||||
return r.conn.EventChan, r.conn.ErrorChan, nil
|
||||
}
|
||||
|
||||
req, err := buildRequest()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to build request: %w", err)
|
||||
}
|
||||
|
||||
conn, err := r.createConnection(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
r.conn = conn
|
||||
|
||||
// Start monitor once
|
||||
r.monitorOnce.Do(func() {
|
||||
go r.monitorConnections()
|
||||
})
|
||||
|
||||
return conn.EventChan, conn.ErrorChan, nil
|
||||
}
|
||||
|
||||
// Get retrieves the current connection
|
||||
func (r *ConnectionRegistry) Get() (*ConnectionMeta, bool) {
|
||||
conn := r.getConnection()
|
||||
return conn, conn != nil
|
||||
}
|
||||
|
||||
// IsConnected checks if there's an active connection
|
||||
func (r *ConnectionRegistry) IsConnected() bool {
|
||||
return r.getConnection() != nil
|
||||
}
|
||||
|
||||
// UpdateLastPing updates the last ping time for the current connection
|
||||
func (r *ConnectionRegistry) UpdateLastPing() {
|
||||
if conn := r.getConnection(); conn != nil {
|
||||
conn.UpdateLastPing()
|
||||
}
|
||||
}
|
||||
|
||||
// Close gracefully shuts down the registry
|
||||
func (r *ConnectionRegistry) Close() {
|
||||
// Stop monitor first
|
||||
select {
|
||||
case <-r.monitorStop:
|
||||
// Already closed
|
||||
default:
|
||||
close(r.monitorStop)
|
||||
}
|
||||
|
||||
// Close connection
|
||||
r.mu.Lock()
|
||||
if r.conn != nil {
|
||||
r.conn.Cancel()
|
||||
r.conn = nil
|
||||
}
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
// getConnection returns the current connection without locking
|
||||
func (r *ConnectionRegistry) getConnection() *ConnectionMeta {
|
||||
r.mu.RLock()
|
||||
conn := r.conn
|
||||
r.mu.RUnlock()
|
||||
return conn
|
||||
}
|
||||
|
||||
// createConnection creates a new SSE connection
|
||||
func (r *ConnectionRegistry) createConnection(req *http.Request) (*ConnectionMeta, error) {
|
||||
ctx, cancel := context.WithCancel(r.ctx)
|
||||
|
||||
eventChan, errorChan, err := r.client.Connect(ctx, req)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("failed to connect: %w", err)
|
||||
}
|
||||
|
||||
meta := &ConnectionMeta{
|
||||
EventChan: eventChan,
|
||||
ErrorChan: errorChan,
|
||||
LastPingAt: time.Now(),
|
||||
EventChan: eventChan,
|
||||
ErrorChan: errorChan,
|
||||
cancel: cancel,
|
||||
}
|
||||
meta.UpdateLastPing()
|
||||
|
||||
r.meta = meta
|
||||
|
||||
// Start cleanup monitor for this connection (NON-BLOCKING)
|
||||
go r.monitor(meta)
|
||||
|
||||
println("Creating new connection\n")
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
// Get retrieves the existing connection
|
||||
func (r *ConnectionRegistry) Get() (*ConnectionMeta, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.meta, r.meta != nil
|
||||
}
|
||||
// monitorConnections checks connection health periodically
|
||||
func (r *ConnectionRegistry) monitorConnections() {
|
||||
const (
|
||||
checkInterval = 30 * time.Second
|
||||
pingTimeout = 2 * time.Minute
|
||||
)
|
||||
|
||||
// Close closes the connection
|
||||
func (r *ConnectionRegistry) Close() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.meta != nil {
|
||||
if r.meta.Cancel != nil {
|
||||
r.meta.Cancel()
|
||||
}
|
||||
r.meta = nil
|
||||
}
|
||||
|
||||
// Cancel the monitor
|
||||
if r.monitorCancel != nil {
|
||||
r.monitorCancel()
|
||||
}
|
||||
}
|
||||
|
||||
// IsConnected returns whether there's an active connection
|
||||
func (r *ConnectionRegistry) IsConnected() bool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.meta != nil
|
||||
}
|
||||
|
||||
// UpdateLastPing updates the last ping time
|
||||
func (r *ConnectionRegistry) UpdateLastPing() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.meta != nil {
|
||||
r.meta.LastPingAt = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// monitor watches for connection closure and cleans up
|
||||
func (r *ConnectionRegistry) monitor(meta *ConnectionMeta) {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
ticker := time.NewTicker(checkInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.monitorCtx.Done():
|
||||
// Context cancelled, exit monitor
|
||||
case <-r.monitorStop:
|
||||
return
|
||||
case <-r.ctx.Done():
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
r.mu.RLock()
|
||||
currentMeta := r.meta
|
||||
r.mu.RUnlock()
|
||||
|
||||
// Check if this monitor is still relevant
|
||||
if currentMeta != meta {
|
||||
// This connection has been replaced, exit monitor
|
||||
return
|
||||
}
|
||||
|
||||
if currentMeta != nil && time.Since(currentMeta.LastPingAt) > 2*time.Minute {
|
||||
fmt.Println("Last ping was more than 2 minutes ago, closing connection")
|
||||
r.mu.Lock()
|
||||
if r.meta == meta { // Double-check under lock
|
||||
if r.meta.Cancel != nil {
|
||||
r.meta.Cancel()
|
||||
}
|
||||
r.meta = nil
|
||||
}
|
||||
r.mu.Unlock()
|
||||
return // Exit monitor after cleanup
|
||||
}
|
||||
r.checkConnectionHealth(pingTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe provides a convenient way to get events from the connection
|
||||
func (r *ConnectionRegistry) Subscribe(build func() (*http.Request, error)) (<-chan SSEEvent, <-chan error, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
// Get existing connection if available
|
||||
if r.meta != nil {
|
||||
return r.meta.EventChan, r.meta.ErrorChan, nil
|
||||
// checkConnectionHealth verifies connection is still alive
|
||||
func (r *ConnectionRegistry) checkConnectionHealth(timeout time.Duration) {
|
||||
conn := r.getConnection()
|
||||
if conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
req, err := build()
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
if time.Since(conn.LastPing()) > timeout {
|
||||
// Connection is stale, close it
|
||||
r.mu.Lock()
|
||||
if r.conn == conn { // Verify it's still the same connection
|
||||
r.conn.Cancel()
|
||||
r.conn = nil
|
||||
}
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
// Create new connection if none exists
|
||||
meta, err := r.create(req)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return meta.EventChan, meta.ErrorChan, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user