@@ -17,6 +17,7 @@ import (
1717 "github.com/psviderski/uncloud/pkg/api"
1818)
1919
20+ // TODO: change upstreams from 'to' to the directive arguments.
2021const caddyfileTemplate = `http:// {
2122 handle {{.VerifyPath}} {
2223 respond "{{.VerifyResponse}}" 200
@@ -75,6 +76,7 @@ func NewCaddyfileGenerator(machineID string, validator CaddyfileValidator, log *
7576}
7677
7778// Generate creates a Caddyfile configuration based on the provided service containers.
79+ // The Caddyfile is generated from the service ports of the healthy containers.
7880// If a 'caddy' service container is running on this machine and defines a custom Caddy config (x-caddy) in its service
7981// spec, it will be validated and prepended to the generated Caddyfile. Custom Caddy configs (x-caddy) defined in other
8082// service specs are validated and appended to the generated Caddyfile. Invalid configs are logged and skipped to ensure
@@ -92,11 +94,11 @@ func (g *CaddyfileGenerator) Generate(ctx context.Context, records []store.Conta
9294 for i , cr := range records {
9395 containers [i ] = cr .Container
9496 }
95- // Sort containers by service name and container ID to generate a stable Caddyfile.
96- slices .SortFunc (containers , func (a , b api.ServiceContainer ) int {
97+ // Sort containers by service name and creation time to generate a stable Caddyfile.
98+ slices .SortStableFunc (containers , func (a , b api.ServiceContainer ) int {
9799 return cmp .Or (
98100 strings .Compare (a .ServiceName (), b .ServiceName ()),
99- strings . Compare ( a . ID , b . ID ),
101+ a . CreatedTime (). Compare ( b . CreatedTime () ),
100102 )
101103 })
102104
@@ -105,26 +107,38 @@ func (g *CaddyfileGenerator) Generate(ctx context.Context, records []store.Conta
105107 return "" , fmt .Errorf ("generate base Caddyfile from service ports: %w" , err )
106108 }
107109
110+ upstreams := serviceUpstreams (containers )
111+
108112 // Find the 'caddy' service container on this machine. Use the most recent one if multiple exist.
109113 var caddyCtr * api.ServiceContainer
110114 for _ , cr := range records {
111115 if cr .MachineID == g .machineID && cr .Container .ServiceName () == CaddyServiceName &&
112- (caddyCtr == nil || cr .Container .Created > caddyCtr . Created ) {
116+ (caddyCtr == nil || cr .Container .CreatedTime (). Compare ( caddyCtr . CreatedTime ()) > 0 ) {
113117 caddyCtr = & cr .Container
114118 }
115119 }
116120
117- // If the caddy container is running on this machine and has a custom Caddy config, prepend it to the generated
118- // Caddyfile and validate it.
121+ // If the caddy container is running on this machine and has a custom Caddy config (global),
122+ // prepend it to the generated Caddyfile and validate it.
119123 if caddyCtr != nil && caddyCtr .ServiceSpec .CaddyConfig () != "" {
120- // TODO: render the template actions in the Caddy config.
121- caddyfileCandidate := caddyCtr .ServiceSpec .CaddyConfig () + "\n \n " + caddyfile
122-
123- if err = g .validator .Validate (ctx , caddyfileCandidate ); err != nil {
124- g .log .Error ("Custom Caddy config on the caddy container on this machine is invalid, skipping it." ,
124+ // Render the custom global Caddy config as a Go template with the upstreams.
125+ tmplCtx := templateContext {
126+ Name : caddyCtr .ServiceName (),
127+ Upstreams : upstreams ,
128+ }
129+ renderedConfig , err := renderCaddyfile (tmplCtx , caddyCtr .ServiceSpec .CaddyConfig ())
130+ if err != nil {
131+ g .log .Error ("Failed to render template directives in custom global Caddy config, skipping it." ,
125132 "service" , caddyCtr .ServiceName (), "container" , caddyCtr .ID , "err" , err )
126133 } else {
127- caddyfile = caddyfileCandidate
134+ caddyfileCandidate := renderedConfig + "\n \n " + caddyfile
135+
136+ if err = g .validator .Validate (ctx , caddyfileCandidate ); err != nil {
137+ g .log .Error ("Custom global Caddy config is invalid, skipping it." ,
138+ "service" , caddyCtr .ServiceName (), "container" , caddyCtr .ID , "err" , err )
139+ } else {
140+ caddyfile = caddyfileCandidate
141+ }
128142 }
129143 }
130144
@@ -134,7 +148,7 @@ func (g *CaddyfileGenerator) Generate(ctx context.Context, records []store.Conta
134148 latestServiceContainers := make (map [string ]api.ServiceContainer , len (containers ))
135149 for _ , ctr := range containers {
136150 if latest , ok := latestServiceContainers [ctr .ServiceName ()]; ok {
137- if ctr .Created > latest . Created {
151+ if ctr .CreatedTime (). Compare ( latest . CreatedTime ()) > 0 {
138152 latestServiceContainers [ctr .ServiceName ()] = ctr
139153 }
140154 } else {
@@ -143,6 +157,8 @@ func (g *CaddyfileGenerator) Generate(ctx context.Context, records []store.Conta
143157 }
144158 sortedServiceNames := slices .Sorted (maps .Keys (latestServiceContainers ))
145159
160+ // Append a custom Caddy config for each service to the Caddyfile and validate it. If the config for a service
161+ // is invalid, skip it but continue processing other services to ensure the Caddyfile remains valid.
146162 for _ , serviceName := range sortedServiceNames {
147163 // Skip the caddy container as we already processed it.
148164 if serviceName == CaddyServiceName {
@@ -154,29 +170,37 @@ func (g *CaddyfileGenerator) Generate(ctx context.Context, records []store.Conta
154170 continue
155171 }
156172
157- // TODO: render the template actions in the Caddy config.
158- caddyfileCandidate := fmt .Sprintf ("%s\n # Service: %s\n %s\n " ,
159- caddyfile , ctr .ServiceName (), ctr .ServiceSpec .CaddyConfig ())
173+ // Render the template actions in the service's Caddy config.
174+ tmplCtx := templateContext {
175+ Name : serviceName ,
176+ Upstreams : upstreams ,
177+ }
178+ renderedConfig , err := renderCaddyfile (tmplCtx , ctr .ServiceSpec .CaddyConfig ())
179+ if err != nil {
180+ g .log .Error ("Failed to render template directives in custom Caddy config for service, skipping it." ,
181+ "service" , serviceName , "err" , err )
182+ continue
183+ }
160184
185+ caddyfileCandidate := fmt .Sprintf ("%s\n # Service: %s\n %s\n " , caddyfile , serviceName , renderedConfig )
161186 if err = g .validator .Validate (ctx , caddyfileCandidate ); err != nil {
162187 g .log .Error ("Custom Caddy config for service is invalid, skipping it." ,
163- "service" , ctr .ServiceName (), "err" , err )
164- continue
188+ "service" , serviceName , "err" , err )
189+ } else {
190+ caddyfile = caddyfileCandidate
165191 }
166-
167- caddyfile = caddyfileCandidate
168192 }
169193
170194 return caddyfile , nil
171195}
172196
173197func (g * CaddyfileGenerator ) generateBaseFromPorts (containers []api.ServiceContainer ) (string , error ) {
174- httpHostUpstreams , httpsHostUpstreams := httpUpstreamsFromContainers (containers )
198+ httpHostUpstreams , httpsHostUpstreams := httpUpstreamsFromPorts (containers )
175199
176200 funcs := template.FuncMap {"join" : strings .Join }
177201 tmpl , err := template .New ("Caddyfile" ).Funcs (funcs ).Parse (caddyfileTemplate )
178202 if err != nil {
179- return "" , fmt .Errorf ("failed to parse Caddyfile template: %w" , err )
203+ return "" , fmt .Errorf ("parse Caddyfile template: %w" , err )
180204 }
181205
182206 data := struct {
@@ -193,15 +217,15 @@ func (g *CaddyfileGenerator) generateBaseFromPorts(containers []api.ServiceConta
193217
194218 var buf bytes.Buffer
195219 if err = tmpl .Execute (& buf , data ); err != nil {
196- return "" , fmt .Errorf ("failed to execute Caddyfile template: %w" , err )
220+ return "" , fmt .Errorf ("execute Caddyfile template: %w" , err )
197221 }
198222
199223 return buf .String (), nil
200224}
201225
202- // httpUpstreamsFromContainers extracts upstreams for HTTP and HTTPS protocols from the published ports of the provided
226+ // httpUpstreamsFromPorts extracts upstreams for HTTP and HTTPS protocols from the published ports of the provided
203227// service containers. It's expected that all containers are healthy.
204- func httpUpstreamsFromContainers (containers []api.ServiceContainer ) (map [string ][]string , map [string ][]string ) {
228+ func httpUpstreamsFromPorts (containers []api.ServiceContainer ) (map [string ][]string , map [string ][]string ) {
205229 // Maps hostnames to lists of upstreams (container IP:port pairs).
206230 httpHostUpstreams := make (map [string ][]string )
207231 httpsHostUpstreams := make (map [string ][]string )
@@ -241,3 +265,40 @@ func httpUpstreamsFromContainers(containers []api.ServiceContainer) (map[string]
241265
242266 return httpHostUpstreams , httpsHostUpstreams
243267}
268+
269+ // serviceUpstreams creates a map of service names to their container IPs.
270+ // Only includes containers connected to the uncloud Docker network.
271+ func serviceUpstreams (containers []api.ServiceContainer ) map [string ][]string {
272+ upstreams := make (map [string ][]string )
273+ for _ , ctr := range containers {
274+ ip := ctr .UncloudNetworkIP ()
275+ if ! ip .IsValid () {
276+ // Container is not connected to the uncloud Docker network (could be host network).
277+ continue
278+ }
279+
280+ serviceName := ctr .ServiceName ()
281+ upstreams [serviceName ] = append (upstreams [serviceName ], ip .String ())
282+ }
283+
284+ return upstreams
285+ }
286+
287+ // renderCaddyfile renders a Caddyfile template with the upstreams function and data.
288+ func renderCaddyfile (tmplCtx templateContext , caddyfile string ) (string , error ) {
289+ funcs := template.FuncMap {
290+ "upstreams" : upstreamsTemplateFn (tmplCtx ),
291+ }
292+
293+ tmpl , err := template .New ("Caddyfile" ).Funcs (funcs ).Parse (caddyfile )
294+ if err != nil {
295+ return "" , fmt .Errorf ("parse config as Go template: %w" , err )
296+ }
297+
298+ var buf bytes.Buffer
299+ if err = tmpl .Execute (& buf , tmplCtx ); err != nil {
300+ return "" , fmt .Errorf ("execute template: %w" , err )
301+ }
302+
303+ return buf .String (), nil
304+ }
0 commit comments