feat(httpapi): return replica docker install commands on speaker create
quality / commitlint (push) Skipped
quality / changes (push) Successful in 8s
quality / docker-check (push) Skipped
quality / openapi (push) Failing after 21s
quality / web (push) Successful in 55s
quality / go (push) Successful in 1m2s
quality / bird2 (push) Successful in 16s
CD / quality (push) Failing after 2m49s
CD / publish (push) Skipped

После создания реплики 201 отдаёт agent_secret, node_token и install.docker_commands (bird2 + agent + Traefik DNS-01). UI показывает шаг установки вместо закрытия диалога, чтобы секрет больше не терялся.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-21 13:51:06 +07:00
co-authored by Cursor
parent c5148ac4a0
commit 3723ba7ed1
11 changed files with 891 additions and 112 deletions
+88
View File
@@ -1,17 +1,32 @@
package httpapi
import (
"bytes"
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"evobgp/internal/nodedispatch"
"evobgp/internal/speakerinstall"
"evobgp/internal/store"
)
var errSpeakerMetaJSONType = errors.New("meta_json must be a JSON object or string")
type speakerCreateRequest struct {
Role string `json:"role"`
Endpoint string `json:"endpoint"`
MetaJSON json.RawMessage `json:"meta_json"`
LetsEncryptEmail string `json:"letsencrypt_email"`
CFDNSAPIToken string `json:"cf_dns_api_token"`
PanelIPWhitelist string `json:"panel_ip_whitelist"`
ControlPlaneURL string `json:"control_plane_url"`
}
func speakerJSONFromStore(st store.Backend, sp *store.Speaker) map[string]any {
if sp == nil {
return map[string]any{}
@@ -123,6 +138,79 @@ func normalizeSpeakerCreate(in *store.Speaker) error {
return nil
}
func metaJSONRawToString(raw json.RawMessage) (string, error) {
t := bytes.TrimSpace(raw)
if len(t) == 0 {
return "{}", nil
}
switch t[0] {
case '"':
var s string
if err := json.Unmarshal(t, &s); err != nil {
return "", err
}
s = strings.TrimSpace(s)
if s == "" {
return "{}", nil
}
return s, nil
case '{':
return string(t), nil
default:
return "", errSpeakerMetaJSONType
}
}
func publicControlPlaneURL(r *http.Request, override string) string {
if s := strings.TrimSpace(override); s != "" {
return strings.TrimRight(s, "/")
}
if origin := strings.TrimSpace(r.Header.Get("Origin")); strings.HasPrefix(origin, "http://") || strings.HasPrefix(origin, "https://") {
return strings.TrimRight(origin, "/")
}
proto := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto"))
if proto == "" {
proto = "https"
}
host := strings.TrimSpace(r.Header.Get("X-Forwarded-Host"))
if i := strings.Index(host, ","); i >= 0 {
host = strings.TrimSpace(host[:i])
}
if host == "" {
host = strings.TrimSpace(r.Host)
}
if host == "" {
return ""
}
return proto + "://" + host
}
func (s *Server) attachReplicaInstall(resp map[string]any, sp *store.Speaker, nodeToken string, req speakerCreateRequest, r *http.Request) {
if s == nil || sp == nil || resp == nil {
return
}
meta := store.ParseSpeakerMeta(sp.MetaJSON)
built, err := speakerinstall.Build(speakerinstall.Params{
SpeakerID: sp.ID,
AgentSecret: meta.AgentSecret,
NodeToken: nodeToken,
BundlePubkey: s.BundlePublicKeyBase64(),
ControlPlaneURL: publicControlPlaneURL(r, req.ControlPlaneURL),
AgentDomain: meta.AgentDomain,
LetsEncryptEmail: req.LetsEncryptEmail,
CFDNSAPIToken: req.CFDNSAPIToken,
PanelIPWhitelist: req.PanelIPWhitelist,
})
if err != nil {
return
}
resp["bundle_pubkey_base64"] = s.BundlePublicKeyBase64()
resp["install"] = map[string]any{
"docker_commands": built.DockerCommands,
"compose_yaml": built.ComposeYAML,
}
}
func (s *Server) recordSpeakerDispatch(tenantID string, sp *store.Speaker, res nodedispatch.Result) {
if s == nil || s.store == nil || sp == nil {
return