@@ -47,10 +47,18 @@ type Manifest struct {
4747}
4848
4949type RenderSpec struct {
50- Env map [string ]string `json:"env"`
51- ComposeValues map [string ]any `json:"compose_values"`
52- Exposure map [string ]any `json:"exposure"`
53- Files []any `json:"files"`
50+ Env map [string ]string `json:"env"`
51+ ComposeValues map [string ]any `json:"compose_values"`
52+ Exposures []TemplateExposure `json:"exposures"`
53+ Files []any `json:"files"`
54+ }
55+
56+ type TemplateExposure struct {
57+ Label string `json:"label"`
58+ Service string `json:"service"`
59+ Port int `json:"port"`
60+ Protocol string `json:"protocol"`
61+ Default bool `json:"default,omitempty"`
5462}
5563
5664type Source struct {
@@ -76,24 +84,25 @@ type RenderRequest struct {
7684}
7785
7886type RenderedTemplate struct {
79- TemplateKey string
80- ProjectName string
81- Compose string
82- ResolvedEnv map [string ]any
83- SecretRefs []string
84- Metadata map [string ]any
85- ExposureIntent map [string ]any
86- RenderExposure map [ string ] any
87- Manifest Manifest
87+ TemplateKey string
88+ ProjectName string
89+ Compose string
90+ ResolvedEnv map [string ]any
91+ SecretRefs []string
92+ Metadata map [string ]any
93+ ExposureIntent map [string ]any
94+ RenderExposures [] TemplateExposure
95+ Manifest Manifest
8896}
8997
9098type DescribeResponse struct {
91- TemplateKey string `json:"templateKey"`
92- Manifest Manifest `json:"manifest"`
93- Inputs []Field `json:"inputs"`
94- Source Source `json:"source"`
95- Exposure map [string ]any `json:"exposure,omitempty"`
96- ComposeValues map [string ]any `json:"composeValues,omitempty"`
99+ TemplateKey string `json:"templateKey"`
100+ Manifest Manifest `json:"manifest"`
101+ Inputs []Field `json:"inputs"`
102+ Source Source `json:"source"`
103+ Exposure map [string ]any `json:"exposure,omitempty"`
104+ Exposures []TemplateExposure `json:"exposures,omitempty"`
105+ ComposeValues map [string ]any `json:"composeValues,omitempty"`
97106}
98107
99108type Service struct {
@@ -166,18 +175,14 @@ func (s *Service) Render(app core.App, request RenderRequest) (*RenderedTemplate
166175 return nil , err
167176 }
168177 result := & RenderedTemplate {
169- TemplateKey : request .TemplateKey ,
170- ProjectName : projectName ,
171- Compose : compose ,
172- ResolvedEnv : resolvedEnv ,
173- SecretRefs : secretRefs ,
174- RenderExposure : cloneAnyMap (tpl .Render .Exposure ),
175- Manifest : tpl .Manifest ,
176- ExposureIntent : map [string ]any {
177- "exposure_type" : normalizedExposureType (tpl .Render .Exposure ),
178- "is_primary" : true ,
179- "target_port" : tpl .Render .Exposure ["targetPort" ],
180- },
178+ TemplateKey : request .TemplateKey ,
179+ ProjectName : projectName ,
180+ Compose : compose ,
181+ ResolvedEnv : resolvedEnv ,
182+ SecretRefs : secretRefs ,
183+ RenderExposures : cloneTemplateExposures (tpl .Render .Exposures ),
184+ Manifest : tpl .Manifest ,
185+ ExposureIntent : exposureIntentFromTemplateExposures (tpl .Render .Exposures ),
181186 Metadata : map [string ]any {
182187 "candidate_kind" : "store-prefill" ,
183188 "prefill_context" : map [string ]any {
@@ -190,6 +195,7 @@ func (s *Service) Render(app core.App, request RenderRequest) (*RenderedTemplate
190195 "template_revision" : tpl .Source .TemplateRevision ,
191196 "origin_kind" : tpl .Source .OriginKind ,
192197 "origin_ref" : tpl .Source .OriginRef ,
198+ "exposures" : templateExposuresToMaps (tpl .Render .Exposures ),
193199 "input_values" : values ,
194200 "secret_refs" : secretRefs ,
195201 },
@@ -211,7 +217,8 @@ func (s *Service) Describe(templateKey string) (*DescribeResponse, error) {
211217 Manifest : tpl .Manifest ,
212218 Inputs : append ([]Field (nil ), tpl .Inputs .Fields ... ),
213219 Source : tpl .Source ,
214- Exposure : cloneAnyMap (tpl .Render .Exposure ),
220+ Exposure : legacyExposureFromTemplateExposures (tpl .Render .Exposures ),
221+ Exposures : cloneTemplateExposures (tpl .Render .Exposures ),
215222 ComposeValues : cloneAnyMap (tpl .Render .ComposeValues ),
216223 }, nil
217224}
@@ -331,22 +338,6 @@ func renderCompose(base string, resolvedEnv map[string]any) (string, error) {
331338 if err := yaml .Unmarshal ([]byte (base ), & node ); err != nil {
332339 return "" , fmt .Errorf ("invalid compose yaml: %w" , err )
333340 }
334- visitAndReplace (& node , func (value string ) (string , bool , error ) {
335- if ! strings .Contains (value , "${" ) {
336- return value , false , nil
337- }
338- result := placeholderPattern .ReplaceAllStringFunc (value , func (raw string ) string {
339- token := strings .TrimSuffix (strings .TrimPrefix (raw , "${" ), "}" )
340- if envValue , ok := resolvedEnv [token ]; ok {
341- return stringify (envValue )
342- }
343- return raw
344- })
345- if strings .Contains (result , "${" ) {
346- return "" , false , fmt .Errorf ("unresolved compose placeholder in %q" , value )
347- }
348- return result , true , nil
349- })
350341 if err := visitAndReplace (& node , func (value string ) (string , bool , error ) {
351342 if ! strings .Contains (value , "${" ) {
352343 return value , false , nil
@@ -432,16 +423,67 @@ func requirementBytes(requirements map[string]any) int64 {
432423 return 0
433424}
434425
435- func normalizedExposureType (exposure map [string ]any ) string {
436- kind := strings .TrimSpace (fmt .Sprint (exposure ["kind" ]))
437- switch kind {
438- case "http" , "https" , "tcp" , "port" :
439- return "port"
440- case "internal_only" :
441- return "internal_only"
442- default :
443- return kind
426+ func exposureIntentFromTemplateExposures (exposures []TemplateExposure ) map [string ]any {
427+ for _ , exposure := range exposures {
428+ if exposure .Default || len (exposures ) == 1 {
429+ if exposure .Port <= 0 {
430+ return nil
431+ }
432+ return map [string ]any {
433+ "exposure_type" : "port" ,
434+ "is_primary" : true ,
435+ "target_port" : exposure .Port ,
436+ }
437+ }
444438 }
439+ return nil
440+ }
441+
442+ func cloneTemplateExposures (input []TemplateExposure ) []TemplateExposure {
443+ if len (input ) == 0 {
444+ return nil
445+ }
446+ return append ([]TemplateExposure (nil ), input ... )
447+ }
448+
449+ func templateExposuresToMaps (input []TemplateExposure ) []map [string ]any {
450+ if len (input ) == 0 {
451+ return nil
452+ }
453+ result := make ([]map [string ]any , 0 , len (input ))
454+ for _ , exposure := range input {
455+ item := map [string ]any {
456+ "label" : strings .TrimSpace (exposure .Label ),
457+ "service" : strings .TrimSpace (exposure .Service ),
458+ "port" : exposure .Port ,
459+ "protocol" : strings .TrimSpace (exposure .Protocol ),
460+ }
461+ if exposure .Default {
462+ item ["default" ] = true
463+ }
464+ result = append (result , item )
465+ }
466+ return result
467+ }
468+
469+ func legacyExposureFromTemplateExposures (exposures []TemplateExposure ) map [string ]any {
470+ for _ , exposure := range exposures {
471+ if exposure .Default || len (exposures ) == 1 {
472+ if exposure .Port <= 0 {
473+ return nil
474+ }
475+ kind := strings .TrimSpace (exposure .Protocol )
476+ if kind == "" {
477+ kind = "http"
478+ }
479+ return map [string ]any {
480+ "kind" : kind ,
481+ "service" : strings .TrimSpace (exposure .Service ),
482+ "targetPort" : exposure .Port ,
483+ }
484+ }
485+ }
486+ return nil
445487}
446488
447489func uniqueStrings (values []string ) []string {
0 commit comments