3d99940658
Sandbox Deployment Platform — Go control plane + agents, NextJS dashboard, nginx reverse proxy. Cross-compile via Docker; deploy via sshpass to 172.18.136.92 (micro) and 172.18.139.186 (gateway). - control-plane: HTTP API, WS hub, SQLite (modernc.org/sqlite) for progress, .log files for log persistence - agent-micro / agent-gateway: alpine:3.20 + bind-mounted repo, binary exec'd in container, no Dockerfile build step - dashboard: NextJS static export + shadcn/ui components, single WebSocket hook - docker-compose.yml: three services on alpine:latest with docker socket bind for agents - scripts/: build.sh (golang:1.23-alpine cross-compile), deploy.sh, patch-nginx.sh (idempotent nginx splice), ssh wrappers Runtime model: pass-through Bitbucket creds per deploy, never logged or persisted on the agent. Control plane never touches git or docker directly — agents do all the work locally.
113 lines
2.9 KiB
Go
113 lines
2.9 KiB
Go
// Package ws is the WebSocket fan-out for SDP. Two flows:
|
|
//
|
|
// agent --(events)--> /ws/agent (one conn per agent)
|
|
// dashboard client --(subscribe)-> /ws/deployments/{id} (one conn per viewer)
|
|
//
|
|
// On agent connect we record the agent's nodeID. On dashboard connect we
|
|
// register a subscriber for one deployment. Events are best-effort fanned out;
|
|
// a slow client is dropped, not allowed to backpressure the agent.
|
|
package ws
|
|
|
|
import (
|
|
"net/http"
|
|
"sync"
|
|
|
|
"github.com/gorilla/websocket"
|
|
|
|
"github.com/sdp/protocol"
|
|
)
|
|
|
|
var upgrader = websocket.Upgrader{
|
|
CheckOrigin: func(r *http.Request) bool { return true }, // ponytail: internal tool, allow all
|
|
}
|
|
|
|
type Hub struct {
|
|
mu sync.RWMutex
|
|
|
|
// one channel per deployment; nil = no subscribers.
|
|
subs map[string]map[chan protocol.Event]struct{}
|
|
|
|
// one channel per connected agent; keyed by nodeID.
|
|
agents map[string]chan<- []byte // outbound to agent (deploy requests, etc.)
|
|
}
|
|
|
|
func New() *Hub {
|
|
return &Hub{
|
|
subs: make(map[string]map[chan protocol.Event]struct{}),
|
|
agents: make(map[string]chan<- []byte),
|
|
}
|
|
}
|
|
|
|
// Publish fans an event out to all subscribers of that deployment.
|
|
// Non-blocking; drops the event for a subscriber if its buffer is full.
|
|
func (h *Hub) Publish(e protocol.Event) {
|
|
h.mu.RLock()
|
|
subs := h.subs[e.DeploymentID]
|
|
chans := make([]chan protocol.Event, 0, len(subs))
|
|
for c := range subs {
|
|
chans = append(chans, c)
|
|
}
|
|
h.mu.RUnlock()
|
|
for _, c := range chans {
|
|
select {
|
|
case c <- e:
|
|
default:
|
|
// ponytail: drop slow subscriber; they'll reconnect.
|
|
}
|
|
}
|
|
}
|
|
|
|
// Subscribe registers a subscriber channel for one deployment.
|
|
// Returns an unsubscribe func the caller must invoke.
|
|
func (h *Hub) Subscribe(deploymentID string) (chan protocol.Event, func()) {
|
|
ch := make(chan protocol.Event, 64)
|
|
h.mu.Lock()
|
|
if h.subs[deploymentID] == nil {
|
|
h.subs[deploymentID] = make(map[chan protocol.Event]struct{})
|
|
}
|
|
h.subs[deploymentID][ch] = struct{}{}
|
|
h.mu.Unlock()
|
|
return ch, func() {
|
|
h.mu.Lock()
|
|
if subs, ok := h.subs[deploymentID]; ok {
|
|
delete(subs, ch)
|
|
if len(subs) == 0 {
|
|
delete(h.subs, deploymentID)
|
|
}
|
|
}
|
|
h.mu.Unlock()
|
|
close(ch)
|
|
}
|
|
}
|
|
|
|
// RegisterAgent stores the outbound channel for a node. Used when the control
|
|
// plane wants to send something back to an agent (deploy requests, stop, etc.).
|
|
func (h *Hub) RegisterAgent(nodeID string, out chan<- []byte) {
|
|
h.mu.Lock()
|
|
h.agents[nodeID] = out
|
|
h.mu.Unlock()
|
|
}
|
|
|
|
func (h *Hub) UnregisterAgent(nodeID string) {
|
|
h.mu.Lock()
|
|
delete(h.agents, nodeID)
|
|
h.mu.Unlock()
|
|
}
|
|
|
|
// SendToAgent best-effort sends a payload to a connected agent. Returns false
|
|
// if the agent isn't connected or its buffer is full.
|
|
func (h *Hub) SendToAgent(nodeID string, payload []byte) bool {
|
|
h.mu.RLock()
|
|
ch, ok := h.agents[nodeID]
|
|
h.mu.RUnlock()
|
|
if !ok {
|
|
return false
|
|
}
|
|
select {
|
|
case ch <- payload:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|