update controlplane router, router test, and runtime

This commit is contained in:
toki 2026-06-20 08:34:53 +09:00
parent 73929de2bd
commit 1685e493dc
3 changed files with 86 additions and 0 deletions

View file

@ -33,6 +33,7 @@ func newRouterWithStore(cfg config.Config, logger *slog.Logger, store storage.St
HeartbeatWaitSec: cfg.ProtoSocketHeartbeatWait,
}, logger)
runtime := NewRuntimeWithStore(protoServer, store)
runtime.WithSecretResolver(EnvWebhookSecretResolver{})
registerProtoSocketHandlers(protoServer.Dispatcher(), runtime, protoServer)
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {

View file

@ -2,6 +2,9 @@ package controlplane
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"log/slog"
"net/http"
@ -321,6 +324,64 @@ func TestWebhookSubscriptionRegistersFilterWithoutLeakingSecretRef(t *testing.T)
}
}
func TestWebhookSubscriptionEnvSecretRefSignsDelivery(t *testing.T) {
const secret = "router-secret"
t.Setenv("GITO_WEBHOOK_SECRET", secret)
consumer := &fakeWebhookConsumer{}
server := fakeWebhookServer(t, consumer)
defer server.Close()
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
registerBranchListener(t, router, `{"repo_id":"nomadcode","branch":"develop","provider":"forgejo"}`)
subReq := httptest.NewRequest(http.MethodPost, "/api/webhook-subscriptions", bytes.NewBufferString(`{
"name": "nomadcode-develop-wakeup",
"target_url": "`+server.URL+`",
"events": ["branch.updated"],
"repo_id": "nomadcode",
"branch": "develop",
"secret_ref": "env:GITO_WEBHOOK_SECRET"
}`))
subRec := httptest.NewRecorder()
router.ServeHTTP(subRec, subReq)
if subRec.Code != http.StatusCreated {
t.Fatalf("subscription status: got %d body=%s", subRec.Code, subRec.Body.String())
}
pushReq := httptest.NewRequest(http.MethodPost, "/callbacks/forgejo/push?repo_id=nomadcode", bytes.NewBufferString(`{
"ref": "refs/heads/develop",
"before": "111",
"after": "222",
"repository": {"name": "nomadcode", "full_name": "toki/nomadcode"},
"commits": [{"id": "222", "modified": ["agent-roadmap/phase/a/milestones/b.md"]}]
}`))
pushReq.Header.Set("X-Forgejo-Event", "push")
pushReq.Header.Set("X-Forgejo-Delivery", "delivery-1")
pushRec := httptest.NewRecorder()
router.ServeHTTP(pushRec, pushReq)
if pushRec.Code != http.StatusAccepted {
t.Fatalf("push status: got %d body=%s", pushRec.Code, pushRec.Body.String())
}
consumer.mu.Lock()
received := append([]webhookReception(nil), consumer.received...)
consumer.mu.Unlock()
if len(received) != 1 {
t.Fatalf("expected one webhook delivery, got %d", len(received))
}
rec := received[0]
if rec.eventType != "branch.updated" || rec.deliveryID == "" {
t.Fatalf("unexpected delivery headers: event=%q delivery=%q", rec.eventType, rec.deliveryID)
}
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write(rec.bodyRaw)
wantSignature := "sha256=" + hex.EncodeToString(mac.Sum(nil))
if rec.signature != wantSignature {
t.Fatalf("signature: got %q want %q", rec.signature, wantSignature)
}
}
func TestWebhookSubscriptionValidatesTargetAndEvents(t *testing.T) {
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())

View file

@ -11,6 +11,7 @@ import (
"fmt"
"net/http"
"net/url"
"os"
"sort"
"strings"
"sync"
@ -34,6 +35,29 @@ type WebhookSecretResolver interface {
ResolveWebhookSecret(ctx context.Context, secretRef string) (string, bool, error)
}
// EnvWebhookSecretResolver resolves subscription secret refs of the form
// `env:VAR_NAME` from the process environment.
type EnvWebhookSecretResolver struct{}
func (EnvWebhookSecretResolver) ResolveWebhookSecret(_ context.Context, secretRef string) (string, bool, error) {
secretRef = strings.TrimSpace(secretRef)
if !strings.HasPrefix(secretRef, "env:") {
return "", false, nil
}
name := strings.TrimSpace(strings.TrimPrefix(secretRef, "env:"))
if name == "" {
return "", false, fmt.Errorf("env secret ref missing variable name")
}
if strings.ContainsAny(name, " \t\r\n=") {
return "", false, fmt.Errorf("invalid env secret ref %q", secretRef)
}
secret, ok := os.LookupEnv(name)
if !ok || secret == "" {
return "", false, nil
}
return secret, true, nil
}
type Runtime struct {
mu sync.Mutex
watches map[string]BranchWatch