Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <cursoragent@cursor.com>
111 lines
3.2 KiB
Go
111 lines
3.2 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"evobgp/internal/httpclient"
|
|
"evobgp/internal/jobs"
|
|
"evobgp/internal/pipeline"
|
|
"evobgp/internal/store"
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
func TestBootstrapWorkerPreservesExplicitPrivateDOH(t *testing.T) {
|
|
t.Setenv("EVOBGP_NODE_DISPATCH_ENABLED", "0")
|
|
t.Setenv("EVOBGP_BIRDC_SOCKET", "")
|
|
t.Setenv("EVOBGP_CDN_ALLOW_PRIVATE", "0")
|
|
var calls atomic.Int32
|
|
resolver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
calls.Add(1)
|
|
raw, err := base64.RawURLEncoding.DecodeString(r.URL.Query().Get("dns"))
|
|
if err != nil {
|
|
w.WriteHeader(400)
|
|
return
|
|
}
|
|
query := new(dns.Msg)
|
|
if err = query.Unpack(raw); err != nil || len(query.Question) == 0 {
|
|
w.WriteHeader(400)
|
|
return
|
|
}
|
|
answer := new(dns.Msg)
|
|
answer.SetReply(query)
|
|
if query.Question[0].Qtype == dns.TypeA {
|
|
answer.Answer = []dns.RR{&dns.A{Hdr: dns.RR_Header{Name: query.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60}, A: net.IPv4(192, 0, 2, 20)}}
|
|
}
|
|
body, err := answer.Pack()
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/dns-message")
|
|
_, _ = w.Write(body)
|
|
}))
|
|
defer resolver.Close()
|
|
st, reg, _, err := BootstrapWorkers(context.Background(), Options{SeedDemo: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
reg.RequestCancelAll()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if e := reg.Drain(ctx); e != nil {
|
|
t.Error(e)
|
|
}
|
|
}()
|
|
tenant, _, _, _, _ := st.DemoIDs()
|
|
profile, err := st.CreateDohProfile(tenant, &store.DohProfile{Name: "explicit private resolver", URL: resolver.URL})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mod, err := st.CreateModule(tenant, &store.Module{Name: "private-doh", Type: "DOMAINS", Enabled: true, DohProfileIDs: []string{profile.ID}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = st.CreateDomainEntry(tenant, mod.ID, &store.DomainEntry{FQDN: "private-resolver.example.test"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
j, _, err := reg.Enqueue(tenant, jobs.KindModuleRefresh, nil, &mod.ID, map[string]any{"module_id": mod.ID})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
deadline := time.After(5 * time.Second)
|
|
ticker := time.NewTicker(5 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
for {
|
|
snap := j.Snapshot()
|
|
status := snap["status"]
|
|
if status == jobs.StatusSucceeded {
|
|
break
|
|
}
|
|
if status == jobs.StatusFailed || status == jobs.StatusCancelled {
|
|
t.Fatalf("worker refresh failed: %+v", snap)
|
|
}
|
|
select {
|
|
case <-deadline:
|
|
t.Fatal("worker did not finish")
|
|
case <-ticker.C:
|
|
}
|
|
}
|
|
if calls.Load() < 2 {
|
|
t.Fatalf("bootstrap worker blocked private DoH: %d requests", calls.Load())
|
|
}
|
|
// The same general-client choice must not remove CDN's independent ACL.
|
|
cdn := httptest.NewTLSServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { t.Error("private CDN reached") }))
|
|
defer cdn.Close()
|
|
req, err := http.NewRequest(http.MethodGet, cdn.URL, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err = pipeline.UpstreamHTTPDo(context.Background(), httpclient.New(httpclient.DefaultTimeout), req); err == nil {
|
|
t.Fatal("private CDN accepted")
|
|
}
|
|
}
|