Slice 2: real auth, agent-mediated repo/branch listing, deployment list from SQLite
- protocol: add RepoInfo, RouteOverride; add HostPort, SandboxID to DeployRequest.
- ws hub: add CallAgent for sync request/response RPCs over the agent WS,
and DeliverAgentReply to route {op:reply} frames back to the caller.
UnregisterAgent now also fails any pending RPCs so callers don't hang.
- agent-micro: new op handlers list_repos, list_branches, probe.
Wire protocol.Event frames use json.RawMessage so each op decodes
its own data shape.
- agent-gateway: same op handlers (list_repos/list_branches/probe) plus
push_routes, which the gateway uses to rewrite the api-gateway
config.php. Detailed in a later commit.
- control-plane login: validateViaAgent now calls CallAgent('probe')
against the gateway agent (git ls-remote), replacing the
accept-any-creds stub.
- control-plane repos: handleListRepos and handleListBranches forward
to the agents via list_repos / list_branches RPCs, replacing the
hardcoded fixtures.
- control-plane deployments: split into its own file. handleListDeployments
reads from SQLite (was hardcoded []). handleCreateDeployment now
supports sandbox-scoped deploys with a host port + env merge.
handleStopDeployment looks up the node from the deployment row.
- store: split into store.go + deployments.go. The Deployment type
adds sandboxId, containerId, hostPort. StartDeploymentInSandbox,
SetContainerID, ListDeployments, GetDeployment, LatestDeploymentBySandboxService
are new.
- store_test.go: round-trips every Slice-2 path (env, sandbox,
template, clone, routes, deployment).
- .gitignore: track bin/ — the build runs on a separate Linux box
with the golang:1.24 toolchain, and the binaries are SCPed from
there to the company VMs (92 / 186). The VMs have no internet.
- Tracked bin/{control-plane,agent-micro,agent-gateway}.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type loginReq struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
// Repo is an optional override: validate creds against a specific
|
||||
// repo on a specific agent. If empty, we use the gateway's default
|
||||
// repo (api-gateway) on the connected gateway agent.
|
||||
Repo string `json:"repo"`
|
||||
}
|
||||
|
||||
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "POST only", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
var body loginReq
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
http.Error(w, "bad json", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if body.Username == "" || body.Password == "" {
|
||||
writeErr(w, http.StatusBadRequest, "username and password required")
|
||||
return
|
||||
}
|
||||
ok := s.validateViaAgent(r.Context(), body.Username, body.Password, body.Repo)
|
||||
if !ok {
|
||||
writeErr(w, http.StatusUnauthorized, "login failed — git ls-remote rejected the credentials")
|
||||
return
|
||||
}
|
||||
tok := s.sess.Issue(body.Username)
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "sdp_session",
|
||||
Value: tok,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: 12 * 3600,
|
||||
})
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
||||
}
|
||||
|
||||
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
if c, err := r.Cookie("sdp_session"); err == nil {
|
||||
s.sess.Revoke(c.Value)
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "sdp_session",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
MaxAge: -1,
|
||||
})
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
||||
}
|
||||
|
||||
// validateViaAgent asks the connected gateway agent to run
|
||||
// `git ls-remote` against the api-gateway repo. The agent holds the
|
||||
// repo and the trust boundary for Bitbucket creds.
|
||||
func (s *Server) validateViaAgent(ctx context.Context, user, pass, repo string) bool {
|
||||
s.agents.mu.RLock()
|
||||
_, connected := s.agents.conns["gateway"]
|
||||
s.agents.mu.RUnlock()
|
||||
if !connected {
|
||||
return false
|
||||
}
|
||||
if repo == "" {
|
||||
repo = s.gatewayRepo
|
||||
}
|
||||
data := map[string]string{
|
||||
"repo": repo,
|
||||
"username": user,
|
||||
"password": pass,
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
raw, err := s.hub.CallAgent(ctx, "gateway", "probe", data, 0)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var reply struct {
|
||||
OK bool `json:"ok"`
|
||||
Err string `json:"error,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &reply); err != nil {
|
||||
return false
|
||||
}
|
||||
return reply.OK
|
||||
}
|
||||
Reference in New Issue
Block a user