Initial SDP skeleton
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.
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
// Command agent-gateway runs on the API Gateway VM (172.18.139.186). It
|
||||
// maintains a WebSocket to the control plane and deploys the gateway
|
||||
// service. The gateway uses the same pipeline as the micro services —
|
||||
// ponytail: same shape, different repo map. If the gateway ever needs a
|
||||
// different build (PHP composer install, etc.), split the deployer package.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/sdp/agent-micro/internal/deployer"
|
||||
"github.com/sdp/agent-micro/internal/gitutil"
|
||||
"github.com/sdp/protocol"
|
||||
)
|
||||
|
||||
var repos = map[string]string{
|
||||
"api-gateway": "/home/user/SDP",
|
||||
}
|
||||
|
||||
func main() {
|
||||
cpURL := flag.String("cp", envOr("SDP_CP_URL", "ws://localhost:8080/ws/agent"), "control plane WS URL")
|
||||
nodeID := flag.String("node", envOr("SDP_NODE_ID", "gateway"), "node id sent in WS query")
|
||||
flag.Parse()
|
||||
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||
if err != nil {
|
||||
log.Fatalf("docker client: %v", err)
|
||||
}
|
||||
|
||||
u, _ := url.Parse(*cpURL)
|
||||
q := u.Query()
|
||||
q.Set("node", *nodeID)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
out := make(chan []byte, 128)
|
||||
var connMu sync.Mutex
|
||||
var conn *websocket.Conn
|
||||
|
||||
go writer(&conn, &connMu, out)
|
||||
|
||||
for {
|
||||
c, err := dial(u)
|
||||
if err != nil {
|
||||
log.Printf("dial: %v; retrying in 2s", err)
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
connMu.Lock()
|
||||
conn = c
|
||||
connMu.Unlock()
|
||||
log.Printf("agent-gateway connected as %s", *nodeID)
|
||||
|
||||
readLoop(c, cli, out, &connMu, &conn)
|
||||
}
|
||||
}
|
||||
|
||||
func dial(u *url.URL) (*websocket.Conn, error) {
|
||||
log.Printf("connecting to %s", u)
|
||||
return websocket.DefaultDialer.Dial(u.String(), nil)
|
||||
}
|
||||
|
||||
func writer(conn **websocket.Conn, mu *sync.Mutex, out <-chan []byte) {
|
||||
for msg := range out {
|
||||
mu.Lock()
|
||||
c := *conn
|
||||
mu.Unlock()
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
if err := c.WriteMessage(websocket.TextMessage, msg); err != nil {
|
||||
log.Printf("write: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type runState struct {
|
||||
deployer *deployer.Deployer
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func readLoop(c *websocket.Conn, cli *dockerclient.Client, out chan<- []byte, mu *sync.Mutex, connPtr **websocket.Conn) {
|
||||
inflight := map[string]*runState{}
|
||||
for {
|
||||
_, raw, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("read: %v", err)
|
||||
mu.Lock()
|
||||
if *connPtr == c {
|
||||
*connPtr = nil
|
||||
}
|
||||
mu.Unlock()
|
||||
_ = c.Close()
|
||||
return
|
||||
}
|
||||
var frame struct {
|
||||
Op string `json:"op"`
|
||||
Data protocol.DeployRequest `json:"data"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &frame); err != nil {
|
||||
log.Printf("bad frame: %v", err)
|
||||
continue
|
||||
}
|
||||
switch frame.Op {
|
||||
case "deploy":
|
||||
repoPath, ok := repos[frame.Data.Repository]
|
||||
if !ok {
|
||||
emit(out, protocol.Event{
|
||||
DeploymentID: frame.Data.DeploymentID,
|
||||
Kind: "status",
|
||||
State: "FAILED",
|
||||
At: time.Now().UnixMilli(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
d := deployer.New(cli, frame.Data.DeploymentID,
|
||||
frame.Data.Repository, repoPath,
|
||||
frame.Data.Branch, frame.Data.Env,
|
||||
gitutil.Creds{Username: frame.Data.Username, Password: frame.Data.Password},
|
||||
)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
inflight[frame.Data.DeploymentID] = &runState{deployer: d, cancel: cancel}
|
||||
go runDeploy(d, ctx, out)
|
||||
case "stop":
|
||||
if rs, ok := inflight[frame.ID]; ok {
|
||||
_ = rs.deployer.Stop(context.Background())
|
||||
rs.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runDeploy(d *deployer.Deployer, ctx context.Context, out chan<- []byte) {
|
||||
events := make(chan protocol.Event, 64)
|
||||
go func() {
|
||||
state := d.Run(ctx, events)
|
||||
if state == "RUNNING" {
|
||||
d.StreamLogs(ctx, events)
|
||||
}
|
||||
close(events)
|
||||
}()
|
||||
for e := range events {
|
||||
emit(out, e)
|
||||
}
|
||||
}
|
||||
|
||||
func emit(out chan<- []byte, e protocol.Event) {
|
||||
b, _ := json.Marshal(e)
|
||||
select {
|
||||
case out <- b:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func envOr(k, def string) string {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
module github.com/sdp/agent-gateway
|
||||
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/docker/docker/client v0.0.0-00010101000000-000000000000
|
||||
github.com/sdp/protocol v0.0.0
|
||||
)
|
||||
Reference in New Issue
Block a user