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
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:
@@ -0,0 +1,254 @@
|
||||
// Package speakerinstall generates a one-shot docker compose snippet for a replica node
|
||||
// (bird2 + evobgp-agent + Traefik Let's Encrypt DNS-01), matching
|
||||
// deploy/compose/docker-compose.remote-speaker.yaml production services.
|
||||
package speakerinstall
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultRegistry = "git.shx.one/denozord"
|
||||
defaultImageTag = "latest"
|
||||
|
||||
placeholderAgentDomain = "CHANGE_ME_AGENT_DOMAIN"
|
||||
placeholderLetsEncryptEmail = "CHANGE_ME_LETSENCRYPT_EMAIL"
|
||||
placeholderCFDNSToken = "CHANGE_ME_CF_DNS_API_TOKEN"
|
||||
placeholderPanelIP = "CHANGE_ME_PANEL_IP"
|
||||
placeholderControlPlaneURL = "CHANGE_ME_CONTROL_PLANE_URL"
|
||||
)
|
||||
|
||||
// Params are values baked into the one-shot compose (not stored on the speaker row).
|
||||
type Params struct {
|
||||
Registry string
|
||||
ImageTag string
|
||||
SpeakerID string
|
||||
AgentSecret string
|
||||
NodeToken string
|
||||
BundlePubkey string
|
||||
ControlPlaneURL string
|
||||
AgentDomain string
|
||||
LetsEncryptEmail string
|
||||
CFDNSAPIToken string
|
||||
PanelIPWhitelist string
|
||||
}
|
||||
|
||||
// Result is the pasteable install payload for POST /v1/speakers 201.
|
||||
type Result struct {
|
||||
ComposeYAML string
|
||||
DockerCommands string
|
||||
}
|
||||
|
||||
type renderData struct {
|
||||
BirdImage string
|
||||
AgentImage string
|
||||
SpeakerID string
|
||||
AgentSecret string
|
||||
NodeToken string
|
||||
BundlePubkey string
|
||||
ControlPlaneURL string
|
||||
AgentDomain string
|
||||
LetsEncryptEmail string
|
||||
CFDNSAPIToken string
|
||||
PanelIPWhitelist string
|
||||
}
|
||||
|
||||
const composeTemplate = `# EvoBGP replica: bird2 + evobgp-agent + Traefik (Let's Encrypt DNS-01 / Cloudflare).
|
||||
# Generated by control plane. Do not commit secrets. ACME state: volume evobgp_speaker_traefik_letsencrypt.
|
||||
|
||||
name: evobgp-remote-speaker
|
||||
|
||||
x-logging: &default-logging
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
services:
|
||||
bird2:
|
||||
image: {{.BirdImage}}
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
volumes:
|
||||
- bird_etc:/etc/bird
|
||||
- bird_run:/run/bird
|
||||
logging: *default-logging
|
||||
|
||||
evobgp-agent:
|
||||
image: {{.AgentImage}}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- bird2
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
environment:
|
||||
EVOBGP_AGENT_LISTEN: ":8443"
|
||||
EVOBGP_AGENT_SECRET: {{yamlQuote .AgentSecret}}
|
||||
EVOBGP_CONTROL_PLANE_URL: {{yamlQuote .ControlPlaneURL}}
|
||||
EVOBGP_NODE_TOKEN: {{yamlQuote .NodeToken}}
|
||||
EVOBGP_SPEAKER_ID: {{yamlQuote .SpeakerID}}
|
||||
EVOBGP_BUNDLE_PUBKEY_BASE64: {{yamlQuote .BundlePubkey}}
|
||||
EVOBGP_BIRD_EXTRACT_DIR: /etc/bird
|
||||
EVOBGP_BIRDC_SOCKET: /run/bird/bird.ctl
|
||||
volumes:
|
||||
- bird_etc:/etc/bird
|
||||
- bird_run:/run/bird
|
||||
entrypoint: ["/usr/local/bin/evobgp-agent"]
|
||||
command: ["serve", "-socket=/run/bird/bird.ctl"]
|
||||
networks:
|
||||
- speaker-net
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.evobgp-agent.rule={{traefikHost .AgentDomain}}
|
||||
- traefik.http.routers.evobgp-agent.entrypoints=websecure
|
||||
- traefik.http.routers.evobgp-agent.tls=true
|
||||
- traefik.http.routers.evobgp-agent.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.evobgp-agent.middlewares=panel-ipwhitelist@docker
|
||||
- traefik.http.middlewares.panel-ipwhitelist.ipallowlist.sourcerange={{.PanelIPWhitelist}}
|
||||
- traefik.http.services.evobgp-agent.loadbalancer.server.port=8443
|
||||
logging: *default-logging
|
||||
|
||||
evobgp-edge:
|
||||
image: traefik:latest
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- evobgp-agent
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
environment:
|
||||
DOCKER_API_VERSION: "1.44"
|
||||
CF_DNS_API_TOKEN: {{yamlQuote .CFDNSAPIToken}}
|
||||
command:
|
||||
- --api.dashboard=false
|
||||
- --providers.docker=true
|
||||
- --providers.docker.exposedbydefault=false
|
||||
- --entrypoints.web.address=:80
|
||||
- --entrypoints.websecure.address=:443
|
||||
- --entrypoints.web.http.redirections.entrypoint.to=websecure
|
||||
- --entrypoints.web.http.redirections.entrypoint.scheme=https
|
||||
- --certificatesresolvers.letsencrypt.acme.email={{.LetsEncryptEmail}}
|
||||
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
|
||||
- --certificatesresolvers.letsencrypt.acme.dnschallenge=true
|
||||
- --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare
|
||||
- --certificatesresolvers.letsencrypt.acme.dnschallenge.delaybeforecheck=15
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- traefik_letsencrypt:/letsencrypt
|
||||
networks:
|
||||
- speaker-net
|
||||
logging: *default-logging
|
||||
|
||||
networks:
|
||||
speaker-net:
|
||||
|
||||
volumes:
|
||||
bird_etc:
|
||||
bird_run:
|
||||
traefik_letsencrypt:
|
||||
name: evobgp_speaker_traefik_letsencrypt
|
||||
`
|
||||
|
||||
var composeTpl = template.Must(template.New("remote-speaker").Funcs(template.FuncMap{
|
||||
"yamlQuote": yamlDoubleQuote,
|
||||
"traefikHost": traefikHostRule,
|
||||
}).Parse(composeTemplate))
|
||||
|
||||
// Build returns compose YAML and bash docker_commands for a replica VPS.
|
||||
func Build(p Params) (Result, error) {
|
||||
p = normalize(p)
|
||||
data := renderData{
|
||||
BirdImage: p.Registry + "/evobgp-bird2:" + p.ImageTag,
|
||||
AgentImage: p.Registry + "/evobgp-agent:" + p.ImageTag,
|
||||
SpeakerID: p.SpeakerID,
|
||||
AgentSecret: p.AgentSecret,
|
||||
NodeToken: p.NodeToken,
|
||||
BundlePubkey: p.BundlePubkey,
|
||||
ControlPlaneURL: p.ControlPlaneURL,
|
||||
AgentDomain: p.AgentDomain,
|
||||
LetsEncryptEmail: p.LetsEncryptEmail,
|
||||
CFDNSAPIToken: p.CFDNSAPIToken,
|
||||
PanelIPWhitelist: p.PanelIPWhitelist,
|
||||
}
|
||||
var yaml strings.Builder
|
||||
if err := composeTpl.Execute(&yaml, data); err != nil {
|
||||
return Result{}, err
|
||||
}
|
||||
compose := strings.TrimSpace(yaml.String()) + "\n"
|
||||
return Result{
|
||||
ComposeYAML: compose,
|
||||
DockerCommands: dockerCommands(compose),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalize(p Params) Params {
|
||||
p.Registry = firstNonEmpty(p.Registry, os.Getenv("EVOBGP_REGISTRY"), defaultRegistry)
|
||||
p.ImageTag = firstNonEmpty(p.ImageTag, os.Getenv("EVOBGP_IMAGE_TAG"), defaultImageTag)
|
||||
p.AgentDomain = sanitizeHost(firstNonEmpty(p.AgentDomain, placeholderAgentDomain))
|
||||
p.LetsEncryptEmail = firstNonEmpty(p.LetsEncryptEmail, placeholderLetsEncryptEmail)
|
||||
p.CFDNSAPIToken = firstNonEmpty(p.CFDNSAPIToken, placeholderCFDNSToken)
|
||||
p.PanelIPWhitelist = firstNonEmpty(p.PanelIPWhitelist, placeholderPanelIP)
|
||||
p.ControlPlaneURL = strings.TrimRight(firstNonEmpty(p.ControlPlaneURL, placeholderControlPlaneURL), "/")
|
||||
return p
|
||||
}
|
||||
|
||||
func dockerCommands(composeYAML string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(`# EvoBGP replica: bird2 + agent + Traefik (Let's Encrypt DNS-01 / Cloudflare)
|
||||
# docker login git.shx.one # if images are private
|
||||
set -euo pipefail
|
||||
sysctl -w net.ipv4.ip_forward=1
|
||||
sysctl -w net.ipv6.conf.all.forwarding=1
|
||||
mkdir -p /etc/sysctl.d
|
||||
printf '%s\n' 'net.ipv4.ip_forward=1' 'net.ipv6.conf.all.forwarding=1' > /etc/sysctl.d/99-evobgp-bird.conf
|
||||
mkdir -p /opt/evobgp-speaker
|
||||
cat > /opt/evobgp-speaker/docker-compose.yaml <<'EVOBGP_SPEAKER_COMPOSE_EOF'
|
||||
`)
|
||||
b.WriteString(composeYAML)
|
||||
if !strings.HasSuffix(composeYAML, "\n") {
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
b.WriteString("EVOBGP_SPEAKER_COMPOSE_EOF\n")
|
||||
b.WriteString("cd /opt/evobgp-speaker && docker compose up -d\n")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func traefikHostRule(domain string) string {
|
||||
return "Host(`" + domain + "`)"
|
||||
}
|
||||
|
||||
func yamlDoubleQuote(s string) string {
|
||||
escaped := strings.ReplaceAll(s, `\`, `\\`)
|
||||
escaped = strings.ReplaceAll(escaped, `"`, `\"`)
|
||||
escaped = strings.ReplaceAll(escaped, "\n", `\n`)
|
||||
return `"` + escaped + `"`
|
||||
}
|
||||
|
||||
func sanitizeHost(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.TrimPrefix(s, "https://")
|
||||
s = strings.TrimPrefix(s, "http://")
|
||||
if i := strings.IndexAny(s, "/:"); i >= 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
s = strings.ReplaceAll(s, "`", "")
|
||||
s = strings.ReplaceAll(s, `"`, "")
|
||||
s = strings.ReplaceAll(s, "'", "")
|
||||
if s == "" {
|
||||
return placeholderAgentDomain
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func firstNonEmpty(vals ...string) string {
|
||||
for _, v := range vals {
|
||||
if s := strings.TrimSpace(v); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user