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

Commit 924c5e2

Browse files
authored
Merge pull request #30 from mnottale/init-revamp
init: Copy compose file as is, parse variables and fill settings.
2 parents 50c7fd1 + cbcf815 commit 924c5e2

File tree

3 files changed

+152
-82
lines changed

3 files changed

+152
-82
lines changed

cmd/init.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,21 @@ import (
1010

1111
// initCmd represents the init command
1212
var initCmd = &cobra.Command{
13-
Use: "init <app-name> [-c <compose-files>...]",
13+
Use: "init <app-name> [-c <compose-file>]",
1414
Short: "Initialize an app package in the current working directory",
1515
Args: cobra.ExactArgs(1),
1616
Run: func(cmd *cobra.Command, args []string) {
1717
fmt.Println("init called")
18-
if err := packager.Init(args[0], composeFiles); err != nil {
18+
if err := packager.Init(args[0], composeFile); err != nil {
1919
fmt.Printf("%v\n", err)
2020
os.Exit(1)
2121
}
2222
},
2323
}
2424

25-
var composeFiles []string
25+
var composeFile string
2626

2727
func init() {
2828
rootCmd.AddCommand(initCmd)
29-
initCmd.Flags().StringArrayVarP(&composeFiles, "compose-files", "c", []string{}, "Initial Compose files (optional)")
29+
initCmd.Flags().StringVarP(&composeFile, "compose-file", "c", "", "Initial Compose file (optional)")
3030
}

packager/init.go

Lines changed: 130 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,22 @@ package packager
22

33
import (
44
"fmt"
5+
"io/ioutil"
56
"log"
67
"os"
7-
"os/exec"
88
"os/user"
99
"path"
10+
"strings"
1011

1112
"github.com/docker/lunchbox/types"
1213
"github.com/docker/lunchbox/utils"
14+
"github.com/pkg/errors"
1315
"gopkg.in/yaml.v2"
1416
)
1517

1618
// Init is the entrypoint initialization function.
1719
// It generates a new application package based on the provided parameters.
18-
func Init(name string, composeFiles []string) error {
20+
func Init(name string, composeFile string) error {
1921
if err := utils.ValidateAppName(name); err != nil {
2022
return err
2123
}
@@ -27,17 +29,10 @@ func Init(name string, composeFiles []string) error {
2729
return err
2830
}
2931

30-
merger := NewPythonComposeConfigMerger()
31-
if len(composeFiles) == 0 {
32-
if _, err := os.Stat("./docker-compose.yml"); os.IsNotExist(err) {
33-
log.Println("no compose file detected")
34-
return initFromScratch(name)
35-
} else if err != nil {
36-
return err
37-
}
38-
return initFromComposeFiles(name, []string{"./docker-compose.yml"}, merger)
32+
if composeFile == "" {
33+
return initFromScratch(name)
3934
}
40-
return initFromComposeFiles(name, composeFiles, merger)
35+
return initFromComposeFile(name, composeFile)
4136
}
4237

4338
func initFromScratch(name string) error {
@@ -54,18 +49,135 @@ func initFromScratch(name string) error {
5449
return utils.CreateFileWithData(path.Join(dirName, "settings.yml"), []byte{'\n'})
5550
}
5651

57-
func initFromComposeFiles(name string, composeFiles []string, merger ComposeConfigMerger) error {
52+
func parseEnv(env string, target map[string]string) {
53+
envlines := strings.Split(env, "\n")
54+
for _, l := range envlines {
55+
l = strings.Trim(l, "\r ")
56+
if l == "" || l[0] == '#' {
57+
continue
58+
}
59+
kv := strings.SplitN(l, "=", 2)
60+
if len(kv) != 2 {
61+
continue
62+
}
63+
target[kv[0]] = kv[1]
64+
}
65+
}
66+
67+
func isAlNum(b byte) bool {
68+
return (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9') || b == '_'
69+
}
70+
71+
func extractString(data string, res *[]string) {
72+
for {
73+
dollar := strings.Index(data, "$")
74+
if dollar == -1 || len(data) == dollar+1 {
75+
break
76+
}
77+
if data[dollar+1] == '$' {
78+
data = data[dollar+2:]
79+
continue
80+
}
81+
dollar++
82+
if data[dollar] == '{' {
83+
dollar++
84+
}
85+
start := dollar
86+
for dollar < len(data) && isAlNum(data[dollar]) {
87+
dollar++
88+
}
89+
*res = append(*res, data[start:dollar])
90+
data = data[dollar:]
91+
}
92+
}
93+
94+
func extractRecurseList(node []interface{}, res *[]string) error {
95+
for _, v := range node {
96+
switch vv := v.(type) {
97+
case string:
98+
extractString(vv, res)
99+
case []interface{}:
100+
if err := extractRecurseList(vv, res); err != nil {
101+
return err
102+
}
103+
case map[interface{}]interface{}:
104+
if err := extractRecurse(vv, res); err != nil {
105+
return err
106+
}
107+
}
108+
}
109+
return nil
110+
}
111+
112+
func extractRecurse(node map[interface{}]interface{}, res *[]string) error {
113+
for _, v := range node {
114+
switch vv := v.(type) {
115+
case string:
116+
extractString(vv, res)
117+
case []interface{}:
118+
if err := extractRecurseList(vv, res); err != nil {
119+
return err
120+
}
121+
case map[interface{}]interface{}:
122+
if err := extractRecurse(vv, res); err != nil {
123+
return err
124+
}
125+
}
126+
}
127+
return nil
128+
}
129+
130+
func extractVariables(composeRaw string) ([]string, error) {
131+
compose := make(map[interface{}]interface{})
132+
err := yaml.Unmarshal([]byte(composeRaw), compose)
133+
if err != nil {
134+
return nil, err
135+
}
136+
var res []string
137+
err = extractRecurse(compose, &res)
138+
return res, err
139+
}
140+
141+
func initFromComposeFile(name string, composeFile string) error {
58142
log.Println("init from compose")
59143

60144
dirName := utils.DirNameFromAppName(name)
61-
composeConfig, err := merger.MergeComposeConfig(composeFiles)
145+
composeRaw, err := ioutil.ReadFile(composeFile)
62146
if err != nil {
63-
return err
147+
return errors.Wrap(err, "failed to read compose file")
64148
}
65-
if err = utils.CreateFileWithData(path.Join(dirName, "docker-compose.yml"), composeConfig); err != nil {
66-
return err
149+
settings := make(map[string]string)
150+
envRaw, err := ioutil.ReadFile(path.Join(path.Dir(composeFile), ".env"))
151+
if err == nil {
152+
parseEnv(string(envRaw), settings)
67153
}
68-
return utils.CreateFileWithData(path.Join(dirName, "settings.yml"), []byte{'\n'})
154+
keys, err := extractVariables(string(composeRaw))
155+
if err != nil {
156+
return errors.Wrap(err, "failed to parse compose file")
157+
}
158+
needsFilling := false
159+
for _, k := range keys {
160+
if _, ok := settings[k]; !ok {
161+
settings[k] = "FILL ME"
162+
needsFilling = true
163+
}
164+
}
165+
settingsYAML, err := yaml.Marshal(settings)
166+
if err != nil {
167+
return errors.Wrap(err, "failed to marshal settings")
168+
}
169+
err = ioutil.WriteFile(path.Join(dirName, "docker-compose.yml"), composeRaw, 0644)
170+
if err != nil {
171+
return errors.Wrap(err, "failed to write docker-compose.yml")
172+
}
173+
err = ioutil.WriteFile(path.Join(dirName, "settings.yml"), settingsYAML, 0644)
174+
if err != nil {
175+
return errors.Wrap(err, "failed to write settings.yml")
176+
}
177+
if needsFilling {
178+
fmt.Println("You will need to edit settings.yml to fill in default values.")
179+
}
180+
return nil
69181
}
70182

71183
func composeFileFromScratch() ([]byte, error) {
@@ -99,35 +211,3 @@ func newMetadata(name string) types.AppMetadata {
99211
Author: userName,
100212
}
101213
}
102-
103-
// ComposeConfigMerger is an interface exposing methods to merge
104-
// multiple compose files into one configuration
105-
type ComposeConfigMerger interface {
106-
MergeComposeConfig(composeFiles []string) ([]byte, error)
107-
}
108-
109-
// PythonComposeConfigMerger implements the ComposeConfigMerger interface and
110-
// executes a `docker-compose` command to merge configs
111-
type PythonComposeConfigMerger struct{}
112-
113-
// NewPythonComposeConfigMerger returns a ComposeConfigMerger implementor
114-
func NewPythonComposeConfigMerger() ComposeConfigMerger {
115-
return &PythonComposeConfigMerger{}
116-
}
117-
118-
// MergeComposeConfig takes a list of paths and merges the Compose files
119-
// at those paths into a single configuration
120-
func (m *PythonComposeConfigMerger) MergeComposeConfig(composeFiles []string) ([]byte, error) {
121-
var args []string
122-
for _, filename := range composeFiles {
123-
args = append(args, fmt.Sprintf("--file=%v", filename))
124-
}
125-
args = append(args, "config")
126-
cmd := exec.Command("docker-compose", args...)
127-
cmd.Stderr = nil
128-
out, err := cmd.Output()
129-
if err != nil {
130-
log.Fatalln(string(err.(*exec.ExitError).Stderr))
131-
}
132-
return out, err
133-
}

packager/init_test.go

Lines changed: 18 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ package packager
33
import (
44
"crypto/rand"
55
"encoding/hex"
6-
"fmt"
6+
"io/ioutil"
77
"os"
8+
"path"
89
"testing"
910

1011
"github.com/gotestyourself/gotestyourself/assert"
@@ -23,58 +24,47 @@ func randomName(prefix string) string {
2324
return prefix + hex.EncodeToString(b)
2425
}
2526

26-
type DummyConfigMerger struct{}
27-
28-
func NewDummyConfigMerger() ComposeConfigMerger {
29-
return &DummyConfigMerger{}
30-
}
31-
32-
var dummyComposeData = `
33-
version: '3.6'
34-
services:
35-
foo:
36-
image: bar
37-
command: baz
27+
func TestInitFromComposeFile(t *testing.T) {
28+
composeData := `services:
29+
nginx:
30+
image: nginx:${NGINX_VERSION}
31+
command: nginx $NGINX_ARGS
3832
`
33+
envData := "# some comment\nNGINX_VERSION=latest"
34+
inputDir := randomName("app_input_")
35+
os.Mkdir(inputDir, 0755)
36+
ioutil.WriteFile(path.Join(inputDir, "docker-compose.yml"), []byte(composeData), 0644)
37+
ioutil.WriteFile(path.Join(inputDir, ".env"), []byte(envData), 0644)
38+
defer os.RemoveAll(inputDir)
3939

40-
func (m *DummyConfigMerger) MergeComposeConfig(composeFiles []string) ([]byte, error) {
41-
if composeFiles[0] == "doesnotexist" {
42-
return []byte{}, fmt.Errorf("no file named %q", composeFiles[0])
43-
}
44-
return []byte(dummyComposeData), nil
45-
}
46-
47-
func TestInitFromComposeFiles(t *testing.T) {
4840
testAppName := randomName("app_")
49-
merger := NewDummyConfigMerger()
5041
dirName := utils.DirNameFromAppName(testAppName)
5142
err := os.Mkdir(dirName, 0755)
5243
assert.NilError(t, err)
5344
defer os.RemoveAll(dirName)
5445

55-
err = initFromComposeFiles(testAppName, []string{"docker-compose.yml"}, merger)
46+
err = initFromComposeFile(testAppName, path.Join(inputDir, "docker-compose.yml"))
5647
assert.NilError(t, err)
5748

5849
manifest := fs.Expected(
5950
t,
6051
fs.WithMode(0755),
61-
fs.WithFile("docker-compose.yml", dummyComposeData, fs.WithMode(0644)),
62-
fs.WithFile("settings.yml", "\n", fs.WithMode(0644)),
52+
fs.WithFile("docker-compose.yml", composeData, fs.WithMode(0644)),
53+
fs.WithFile("settings.yml", "NGINX_ARGS: FILL ME\nNGINX_VERSION: latest\n", fs.WithMode(0644)),
6354
)
6455

6556
assert.Assert(t, fs.Equal(dirName, manifest))
6657
}
6758

6859
func TestInitFromInvalidComposeFile(t *testing.T) {
6960
testAppName := randomName("app_")
70-
merger := NewDummyConfigMerger()
7161
dirName := utils.DirNameFromAppName(testAppName)
7262
err := os.Mkdir(dirName, 0755)
7363
assert.NilError(t, err)
7464
defer os.RemoveAll(dirName)
7565

76-
err = initFromComposeFiles(testAppName, []string{"doesnotexist"}, merger)
77-
assert.ErrorContains(t, err, "no file named \"doesnotexist\"")
66+
err = initFromComposeFile(testAppName, "doesnotexist")
67+
assert.ErrorContains(t, err, "failed to read")
7868
}
7969

8070
func TestWriteMetadataFile(t *testing.T) {

0 commit comments

Comments
 (0)