Skip to content

Commit d0e04e8

Browse files
feat(generate,proxy): emit Caddy proxy labels for routed services (N5 part 1, spec 05) (#29)
Wire the N1 route table into generation: proxy.LabelsForService(m, project, service) returns the caddy-docker-proxy labels (caddy=<svc>.<proj>.localhost, caddy.reverse_proxy={{upstreams <port>}}, caddy.tls=internal when httpsLocal) when the workspace declares a Caddy proxy and the service exposes a port; buildProjectService merges them onto the service via maps.Copy. No-op (nil) when the proxy is disabled — so the existing golden + determinism are unchanged and `up` is unaffected unless the user opts in with network.proxy.engine: caddy. This makes the proxy routes actually take effect in compose (adding/removing a service reloads Caddy with no central-config edit). The remaining N5 piece is the saga trust phase + doctor trust/dns probes. Test: a proxy-enabled workspace's generated compose carries the caddy labels. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c659696 commit d0e04e8

3 files changed

Lines changed: 65 additions & 1 deletion

File tree

internal/generate/compose.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package generate
33
import (
44
"context"
55
"fmt"
6+
"maps"
67
"sort"
78
"strings"
89

@@ -11,6 +12,7 @@ import (
1112
"github.com/goccy/go-yaml"
1213

1314
"github.com/open-source-cloud/devstack/internal/config"
15+
"github.com/open-source-cloud/devstack/internal/proxy"
1416
"github.com/open-source-cloud/devstack/internal/template"
1517
)
1618

@@ -92,7 +94,12 @@ func buildProjectService(res *graphResolver, m *config.Model, project, service s
9294
}
9395

9496
out["networks"] = map[string]any{"default": nil, SharedNetwork: nil}
95-
out["labels"] = b.labels(map[string]string{LabelProject: project, LabelService: service})
97+
svcLabels := map[string]string{LabelProject: project, LabelService: service}
98+
// spec 05 — when a reverse proxy is configured, emit the caddy-docker-proxy
99+
// route labels onto the service so adding/removing it reloads Caddy with no
100+
// central-config edit. No-op (nil) when the proxy is disabled.
101+
maps.Copy(svcLabels, proxy.LabelsForService(m, project, service))
102+
out["labels"] = b.labels(svcLabels)
96103

97104
if exp := exposeList(svc.Ports); len(exp) > 0 {
98105
out["expose"] = exp

internal/generate/health_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,38 @@ services:
161161
t.Errorf("compose should reference the cache dependency:\n%s", compose)
162162
}
163163
}
164+
165+
// TestProxyLabelsEmitted verifies the caddy-docker-proxy labels are merged onto
166+
// a routed service when the workspace declares a proxy (spec 05 / N5).
167+
func TestProxyLabelsEmitted(t *testing.T) {
168+
root := t.TempDir()
169+
write := func(rel, body string) {
170+
p := filepath.Join(root, rel)
171+
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
172+
t.Fatal(err)
173+
}
174+
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
175+
t.Fatal(err)
176+
}
177+
}
178+
write("workspace.yaml", "apiVersion: devstack/v1\nkind: Workspace\nname: shop\nnetwork: { proxy: { engine: caddy, httpsLocal: true } }\nprojects:\n - { name: api, path: api }\n")
179+
write("api/devstack.yaml", "apiVersion: devstack/v1\nkind: Project\nname: api\nservices:\n web: { template: node.vite, ports: { http: 8080 } }\n")
180+
m, err := config.LoadAt(root)
181+
if err != nil {
182+
t.Fatalf("load: %v", err)
183+
}
184+
g, err := New(m, template.NewFSSource(templates.FS), WithEnv(map[string]string{}))
185+
if err != nil {
186+
t.Fatalf("New: %v", err)
187+
}
188+
st, err := g.GenerateProject("api")
189+
if err != nil {
190+
t.Fatalf("GenerateProject: %v", err)
191+
}
192+
compose := string(st.Compose)
193+
for _, want := range []string{"caddy: web.api.localhost", "caddy.reverse_proxy", "caddy.tls: internal"} {
194+
if !strings.Contains(compose, want) {
195+
t.Errorf("compose missing proxy label %q:\n%s", want, compose)
196+
}
197+
}
198+
}

internal/proxy/proxy.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,28 @@ func HostFor(service, project string) string {
7171
return service + "." + project + "." + LocalDomain
7272
}
7373

74+
// LabelsForService returns the Caddy labels a generated project service should
75+
// carry when the proxy is enabled and the service exposes a port; nil otherwise.
76+
// This is the single seam generate uses to emit routing (no central config).
77+
func LabelsForService(m *config.Model, project, service string) map[string]string {
78+
if !Enabled(m) {
79+
return nil
80+
}
81+
p, ok := m.Projects[project]
82+
if !ok {
83+
return nil
84+
}
85+
port := primaryPort(p.Services[service].Ports)
86+
if port == 0 {
87+
return nil
88+
}
89+
return CaddyLabels(Route{
90+
Project: project, Service: service,
91+
Host: HostFor(service, project), Port: port,
92+
TLS: m.Workspace.Network.Proxy.HTTPSLocal,
93+
})
94+
}
95+
7496
// CaddyLabels renders the caddy-docker-proxy labels for a route. These are merged
7597
// onto the project service in the generated compose so caddy reloads on the
7698
// Docker event with no central-config edit (spec 05).

0 commit comments

Comments
 (0)