1
1
package render
2
2
3
3
import (
4
+ "fmt"
5
+ "regexp"
4
6
"strings"
5
7
6
8
"github.com/deislabs/cnab-go/bundle"
7
9
"github.com/docker/app/internal/compose"
8
10
"github.com/docker/app/types"
9
11
"github.com/docker/app/types/parameters"
10
12
"github.com/docker/cli/cli/compose/loader"
11
- composetemplate "github.com/docker/cli/cli/compose/template"
12
13
composetypes "github.com/docker/cli/cli/compose/types"
13
14
"github.com/pkg/errors"
14
15
@@ -18,6 +19,23 @@ import (
18
19
_ "github.com/docker/app/internal/formatter/yaml"
19
20
)
20
21
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
+
21
39
// Render renders the Compose file for this app, merging in parameters files, other compose files, and env
22
40
// appname string, composeFiles []string, parametersFiles []string
23
41
func 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
37
55
if err != nil {
38
56
return nil , errors .Wrap (err , "failed to merge parameters" )
39
57
}
40
- configFiles , _ , err := compose .Load (app .Composes ())
58
+ composeContent := string (app .Composes ()[0 ])
59
+ composeContent , err = substituteParams (allParameters .Flatten (), composeContent )
41
60
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
+ }
43
93
}
44
- return render ( app . Path , configFiles , allParameters . Flatten (), imageMap )
94
+ return composeContent , nil
45
95
}
46
96
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
+ }
48
102
rendered , err := loader .Load (composetypes.ConfigDetails {
49
103
WorkingDir : appPath ,
50
104
ConfigFiles : configFiles ,
51
- Environment : finalEnv ,
52
- }, func (opts * loader.Options ) {
53
- opts .Interpolate .Substitute = substitute
54
105
})
55
106
if err != nil {
56
107
return nil , errors .Wrap (err , "failed to load Compose file" )
@@ -67,20 +118,6 @@ func render(appPath string, configFiles []composetypes.ConfigFile, finalEnv map[
67
118
return rendered , nil
68
119
}
69
120
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
-
84
121
func processEnabled (config * composetypes.Config ) error {
85
122
services := []composetypes.ServiceConfig {}
86
123
for _ , service := range config .Services {
0 commit comments