11package render
22
33import (
4+ "fmt"
5+ "regexp"
46 "strings"
57
68 "github.com/deislabs/cnab-go/bundle"
79 "github.com/docker/app/internal/compose"
810 "github.com/docker/app/types"
911 "github.com/docker/app/types/parameters"
1012 "github.com/docker/cli/cli/compose/loader"
11- composetemplate "github.com/docker/cli/cli/compose/template"
1213 composetypes "github.com/docker/cli/cli/compose/types"
1314 "github.com/pkg/errors"
1415
@@ -18,6 +19,23 @@ import (
1819 _ "github.com/docker/app/internal/formatter/yaml"
1920)
2021
22+ // pattern matching for ${text} and $text substrings (characters allowed: 0-9 a-z _ .)
23+ const (
24+ delimiter = `\$`
25+ // variable name must start with at least one of the the following: a-z, A-Z or _
26+ substitution = `[a-zA-Z_]+([a-zA-Z0-9_]*(([.]{1}[0-9a-zA-Z_]+)|([0-9a-zA-Z_])))*`
27+ // compose files may contain variable names followed by default values/error messages with separators ':-', '-', ':?' and '?'.
28+ defaultValuePattern = `[a-zA-Z_]+[a-zA-Z0-9_.]*((:-)|(\-)|(:\?)|(\?)){1}(.*)`
29+ )
30+
31+ var (
32+ patternString = fmt .Sprintf (
33+ `%s(?i:(?P<named>%s)|(?P<skip>%s{1,})|\{(?P<braced>%s)\}|\{(?P<fail>%s)\})` ,
34+ delimiter , substitution , delimiter , substitution , defaultValuePattern ,
35+ )
36+ rePattern = regexp .MustCompile (patternString )
37+ )
38+
2139// Render renders the Compose file for this app, merging in parameters files, other compose files, and env
2240// appname string, composeFiles []string, parametersFiles []string
2341func Render (app * types.App , env map [string ]string , imageMap map [string ]bundle.Image ) (* composetypes.Config , error ) {
@@ -37,20 +55,53 @@ func Render(app *types.App, env map[string]string, imageMap map[string]bundle.Im
3755 if err != nil {
3856 return nil , errors .Wrap (err , "failed to merge parameters" )
3957 }
40- configFiles , _ , err := compose .Load (app .Composes ())
58+ composeContent := string (app .Composes ()[0 ])
59+ composeContent , err = substituteParams (allParameters .Flatten (), composeContent )
4160 if err != nil {
42- return nil , errors .Wrap (err , "failed to load composefiles" )
61+ return nil , err
62+ }
63+ return render (app .Path , composeContent , imageMap )
64+ }
65+
66+ func substituteParams (allParameters map [string ]string , composeContent string ) (string , error ) {
67+ matches := rePattern .FindAllStringSubmatch (composeContent , - 1 )
68+ if len (matches ) == 0 {
69+ return composeContent , nil
70+ }
71+ for _ , match := range matches {
72+ groups := make (map [string ]string )
73+ for i , name := range rePattern .SubexpNames ()[1 :] {
74+ groups [name ] = match [i + 1 ]
75+ }
76+ //fail on default values enclosed within {}
77+ if fail := groups ["fail" ]; fail != "" {
78+ return "" , errors .New (fmt .Sprintf ("Parameters must not have default values set in compose file. Invalid parameter: %s." , match [0 ]))
79+ }
80+ if skip := groups ["skip" ]; skip != "" {
81+ continue
82+ }
83+ varString := match [0 ]
84+ val := groups ["named" ]
85+ if val == "" {
86+ val = groups ["braced" ]
87+ }
88+ if value , ok := allParameters [val ]; ok {
89+ composeContent = strings .ReplaceAll (composeContent , varString , value )
90+ } else {
91+ return "" , errors .New (fmt .Sprintf ("Failed to set value for %s. Value not found in parameters." , val ))
92+ }
4393 }
44- return render ( app . Path , configFiles , allParameters . Flatten (), imageMap )
94+ return composeContent , nil
4595}
4696
47- func render (appPath string , configFiles []composetypes.ConfigFile , finalEnv map [string ]string , imageMap map [string ]bundle.Image ) (* composetypes.Config , error ) {
97+ func render (appPath string , composeContent string , imageMap map [string ]bundle.Image ) (* composetypes.Config , error ) {
98+ configFiles , _ , err := compose .Load ([][]byte {[]byte (composeContent )})
99+ if err != nil {
100+ return nil , errors .Wrap (err , "failed to load compose content" )
101+ }
48102 rendered , err := loader .Load (composetypes.ConfigDetails {
49103 WorkingDir : appPath ,
50104 ConfigFiles : configFiles ,
51- Environment : finalEnv ,
52- }, func (opts * loader.Options ) {
53- opts .Interpolate .Substitute = substitute
54105 })
55106 if err != nil {
56107 return nil , errors .Wrap (err , "failed to load Compose file" )
@@ -67,20 +118,6 @@ func render(appPath string, configFiles []composetypes.ConfigFile, finalEnv map[
67118 return rendered , nil
68119}
69120
70- func substitute (template string , mapping composetemplate.Mapping ) (string , error ) {
71- return composetemplate .SubstituteWith (template , mapping , compose .ExtrapolationPattern , errorIfMissing )
72- }
73-
74- func errorIfMissing (substitution string , mapping composetemplate.Mapping ) (string , bool , error ) {
75- value , found := mapping (substitution )
76- if ! found {
77- return "" , true , & composetemplate.InvalidTemplateError {
78- Template : "required variable " + substitution + " is missing a value" ,
79- }
80- }
81- return value , true , nil
82- }
83-
84121func processEnabled (config * composetypes.Config ) error {
85122 services := []composetypes.ServiceConfig {}
86123 for _ , service := range config .Services {
0 commit comments