Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit 83f43bd

Browse files
APP-179 Simplify templating of docker app (#602)
* parameter substitution using string replace instead of compose functions Signed-off-by: Anca Iordache <[email protected]> Co-authored-by: Silvin Lubecki <[email protected]>
1 parent daee953 commit 83f43bd

File tree

4 files changed

+274
-99
lines changed

4 files changed

+274
-99
lines changed

internal/packager/init.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"os"
99
"os/user"
1010
"path/filepath"
11+
"regexp"
1112
"strings"
1213
"text/template"
1314

@@ -163,7 +164,6 @@ func initFromComposeFile(name string, composeFile string) error {
163164
}
164165
}
165166
}
166-
167167
expandedParams, err := parameters.FromFlatten(params)
168168
if err != nil {
169169
return errors.Wrap(err, "failed to expand parameters")
@@ -172,6 +172,8 @@ func initFromComposeFile(name string, composeFile string) error {
172172
if err != nil {
173173
return errors.Wrap(err, "failed to marshal parameters")
174174
}
175+
// remove parameter default values from compose before saving
176+
composeRaw = removeDefaultValuesFromCompose(composeRaw)
175177
err = ioutil.WriteFile(filepath.Join(dirName, internal.ComposeFileName), composeRaw, 0644)
176178
if err != nil {
177179
return errors.Wrap(err, "failed to write docker-compose.yml")
@@ -186,6 +188,20 @@ func initFromComposeFile(name string, composeFile string) error {
186188
return nil
187189
}
188190

191+
func removeDefaultValuesFromCompose(compose []byte) []byte {
192+
// find variable names followed by default values/error messages with ':-', '-', ':?' and '?' as separators.
193+
rePattern := regexp.MustCompile(`\$\{[a-zA-Z_]+[a-zA-Z0-9_.]*((:-)|(\-)|(:\?)|(\?))(.*)\}`)
194+
matches := rePattern.FindAllSubmatch(compose, -1)
195+
//remove default value from compose content
196+
for _, groups := range matches {
197+
variable := groups[0]
198+
separator := groups[1]
199+
variableName := bytes.SplitN(variable, separator, 2)[0]
200+
compose = bytes.ReplaceAll(compose, variable, []byte(fmt.Sprintf("%s}", variableName)))
201+
}
202+
return compose
203+
}
204+
189205
func composeFileFromScratch() ([]byte, error) {
190206
fileStruct := types.NewInitialComposeFile()
191207
return yaml.Marshal(fileStruct)

internal/packager/init_test.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,17 @@ func TestInitFromComposeFileWithFlattenedParams(t *testing.T) {
4949
version: '3.0'
5050
services:
5151
service1:
52-
image: image1
5352
ports:
5453
- ${ports.service1:-9001}
55-
5654
service2:
57-
image: image2
5855
ports:
59-
- ${ports.service2:-9002}
56+
- ${ports.service2-9002}
57+
service3:
58+
ports:
59+
- ${ports.service3:?'port is unset or empty in the environment'}
60+
service4:
61+
ports:
62+
- ${ports.service4?'port is unset or empty in the environment'}
6063
`
6164
inputDir := fs.NewDir(t, "app_input_",
6265
fs.WithFile(internal.ComposeFileName, composeData),
@@ -75,14 +78,31 @@ services:
7578
const expectedParameters = `ports:
7679
service1: 9001
7780
service2: 9002
81+
service3: FILL ME
82+
service4: FILL ME
83+
`
84+
const expectedUpdatedComposeData = `
85+
version: '3.0'
86+
services:
87+
service1:
88+
ports:
89+
- ${ports.service1}
90+
service2:
91+
ports:
92+
- ${ports.service2}
93+
service3:
94+
ports:
95+
- ${ports.service3}
96+
service4:
97+
ports:
98+
- ${ports.service4}
7899
`
79100
manifest := fs.Expected(
80101
t,
81102
fs.WithMode(0755),
82-
fs.WithFile(internal.ComposeFileName, composeData, fs.WithMode(0644)),
103+
fs.WithFile(internal.ComposeFileName, expectedUpdatedComposeData, fs.WithMode(0644)),
83104
fs.WithFile(internal.ParametersFileName, expectedParameters, fs.WithMode(0644)),
84105
)
85-
86106
assert.Assert(t, fs.Equal(dir.Join(appName), manifest))
87107
}
88108

render/render.go

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package render
22

33
import (
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
2341
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
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-
84121
func processEnabled(config *composetypes.Config) error {
85122
services := []composetypes.ServiceConfig{}
86123
for _, service := range config.Services {

0 commit comments

Comments
 (0)