-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfig.go
337 lines (299 loc) · 11.2 KB
/
config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
package config
import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"github.com/docker/docker/api/types"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/afero"
)
const (
projectShellDefault = "/bin/sh"
registryTimeoutSecondsDefault = 2
registryContinueOnFail = false
// LogLevelDefault is the log level used when one has not been
// specified in an environment variable or in configuration file.
LogLevelDefault = "info"
// NoProjectWarning is the message provided to the user when no project
// could be found
NoProjectWarning = `No docker-compose.yml file in this directory.
If you would like to use dev outside of your project directory, set the DEV_CONFIG
environment variable to point to your project dev.yaml.`
)
// ConfigFileDefaults are the default filenames for the configuration
// file for this program.
var ConfigFileDefaults = []string{".dev.yml", ".dev.yaml", "dev.yml", "dev.yaml"}
// todo: pull these from docker library if we can
var dockerComposeFilenames = []string{"docker-compose.yml", "docker-compose.yaml"}
// Dev is the datastructure into which we unmarshal the dev configuration
// file.
type Dev struct {
Log LogConfig `mapstructure:"log"`
Projects map[string]*Project `mapstructure:"projects"`
Registries map[string]*Registry `mapstructure:"registries"`
// Filename is the full path of the configuration file containing
// this configuration. This is used internally and is ignored
// if specified by the user.
Filename string
// Dir is either the location of the config file or the current
// working directory if there is no config file. This is used
// intenrally and is ignored if specified by the user.
Dir string
// Networks are a list of the networks managed by dev. A network
// will be created automatically as required by dev if it is listed
// as a dependency of your project. These are networks that are used
// as 'external networks' in your docker-compose configuration.
Networks map[string]*types.NetworkCreate `mapstructure:"networks"`
// ImagePrefix is the prefix to add to images built with this
// tool through compose. Compose forces the use of a prefix so we
// allow the configuration of that prefix here. Dev must know the
// prefix in order to perform some image specific operations. If not
// set, this defaults to the directory where the this tool's config
// file is located or the directory or the docker-compose.yml if one is
// found. Note that compose only adds the prefix to local image
// builds.
ImagePrefix string `mapstructure:"image_prefix"`
// Filesystem to read configuration from
fs afero.Fs
}
// LogConfig holds the logging related configuration.
type LogConfig struct {
Level string `mapstructure:"level"`
}
// Project configuration structure. This must be used when using more than one
// docker-compose.yml file for a project.
type Project struct {
// The paths of the docker compose files for this project. Can be
// relative or absolute paths.
DockerComposeFilenames []string `mapstructure:"docker_compose_files"`
// Directory is the full-path to the location of the dev configuration
// file that contains this project configuration.
Directory string `mapstructure:"directory"`
// Ignored if set by user.
Name string `mapstructure:"name"`
// Alternate names for this project.
Aliases []string `mapstructure:"aliases"`
// Whether project should be included for use by this project, default false
Hidden bool `mapstructure:"hidden"`
// Shell used to enter the project container with 'sh' command,
// default is /bin/bash
Shell string `mapstructure:"shell"`
// Projects, registries, networks on which this project depends.
Dependencies []string `mapstructure:"depends_on"`
}
// Registry repesents the configuration required to model a container registry.
// Users can configure their project to be dependent on a registry. When this
// occurs, we will login to the container registry using the configuration
// provided here. This will allow users to host their images in private image
// repos.
type Registry struct {
// User readable name, not used by the docker client
Name string `mapstructure:"name"`
URL string `mapstructure:"url"`
// TODO: other forms of auth exist and should be supported, but this is
// what I need..
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
// Sometimes these can be firewalled, so a default timeout of 2 seconds
// is provided, though can be tweaked here
TimeoutSeconds int64 `mapstructure:"timeout_seconds"`
// if login or connection fails, should dev continue with command or
// fail hard. Default is True
ContinueOnFailure bool `mapstructure:"continue_on_failure"`
}
// NewConfig structs the default configuration structure for dev driven
// projects.
func NewConfig() *Dev {
config := &Dev{
Projects: make(map[string]*Project),
Networks: make(map[string]*types.NetworkCreate),
Registries: make(map[string]*Registry),
Log: LogConfig{
Level: LogLevelDefault,
},
fs: afero.NewOsFs(),
}
return config
}
// SetFs set the filesystem used for reading configuration. Helpful during
// testing.
func (d *Dev) SetFs(fs afero.Fs) {
d.fs = fs
}
// GetFs returns the filesystem implemention. The default uses the filesytem
// but can be replaced via SetFs for testing.
func (d *Dev) GetFs() afero.Fs {
return d.fs
}
func pathToDockerComposeFilenames(directory string) []string {
paths := make([]string, len(dockerComposeFilenames))
for _, filename := range dockerComposeFilenames {
paths = append(paths, path.Join(directory, filename))
}
return paths
}
func directoryContainsDockerComposeConfig(directory string) (bool, string) {
for _, configFile := range pathToDockerComposeFilenames(directory) {
if _, err := os.Stat(configFile); err == nil {
return true, configFile
}
}
return false, ""
}
func projectNameFromPath(projectPath string) string {
return path.Base(projectPath)
}
func newProjectConfig(projectPath, composeFilename string) *Project {
project := &Project{
Directory: projectPath,
Name: projectNameFromPath(projectPath),
DockerComposeFilenames: []string{composeFilename},
}
return project
}
func expandRelativeDirectories(config *Dev) {
for _, project := range config.Projects {
for i, composeFile := range project.DockerComposeFilenames {
if !strings.HasPrefix(composeFile, "/") {
fullPath := path.Clean(path.Join(config.Dir, composeFile))
project.DockerComposeFilenames[i] = fullPath
}
}
}
}
func setDefaults(config *Dev) {
// Need to be smarter here.. Users unable to specify 0 here, which is
// a reasonable default for many values.
for _, registry := range config.Registries {
if registry.TimeoutSeconds == 0 {
registry.TimeoutSeconds = registryTimeoutSecondsDefault
}
}
for _, project := range config.Projects {
if project.Shell == "" {
project.Shell = projectShellDefault
}
}
if config.ImagePrefix == "" {
config.ImagePrefix = filepath.Base(config.Dir)
}
for name, registry := range config.Registries {
registry.Name = name
}
for name, project := range config.Projects {
// if user did not specify a custom project container name,
// the container name is assumed to be the same name as the
// project itself.
if project.Name == "" {
project.Name = name
}
// Have to remember where the project was defined so we can
// figure out relative directories when launching commands. See
// Project.Shell. This is also passed in when parsing docker-compose
// files where it used to load env files.
project.Directory = filepath.Dir(config.Filename)
}
}
// Expand makes modifications to the configuration structure
// provided by the user before it is used by dev.
func Expand(filename string, config *Dev) {
config.Filename = filename
if config.Filename == "" {
dir, err := os.Getwd()
if err != nil {
log.Fatalf("Error getting the current directory: %s", err)
}
config.Dir = dir
} else {
config.Dir = filepath.Dir(config.Filename)
}
expandRelativeDirectories(config)
setDefaults(config)
for name, registry := range config.Registries {
registry.Name = name
}
for name, project := range config.Projects {
// if user did not specify a custom project container name,
// the container name is assumed to be the same name as the
// project itself. Should probably split this out into another
// variable.
if project.Name == "" {
project.Name = name
}
}
// The code below does not work because project.Directory is not set!
// Temporarily working around by only adding projects when a dev config
// file is not found.
if config.Filename == "" {
// If there's a docker-compose file in the current directory
// that's not specified in the config file create a default project
// with the name of the directory.
if hasConfig, filename := directoryContainsDockerComposeConfig(config.Dir); hasConfig {
found := false
for _, project := range config.Projects {
if project.Directory == config.Dir {
found = true
}
}
if !found {
log.Debugf("Creating default project config for project in %s", config.Dir)
project := newProjectConfig(config.Dir, filename)
config.Projects[project.Name] = project
}
}
}
if len(config.Projects) == 0 {
fmt.Println(NoProjectWarning)
}
}
func isDefaultConfig(config *Dev) bool {
return (len(config.Projects) == 0 && len(config.Networks) == 0 &&
len(config.Registries) == 0 && config.ImagePrefix == "")
}
// Merge adds the configuration from source to the configuration from
// target. An error is returned if there is an object with the same name
// in target and source or if the configs cannot be merged for whatever reason.
func Merge(target *Dev, source *Dev) error {
if isDefaultConfig(target) {
// project wide settings are set by the first config listed
target.ImagePrefix = source.ImagePrefix
target.Log.Level = source.Log.Level
target.Dir = source.Dir
target.Filename = source.Filename
} else if source.ImagePrefix != target.ImagePrefix {
// Not sure I like forcing this.. but if users switch back and forth
// between a project that uses multiple configurations and then start
// using one of those with only one configuration, some dev commands
// will not function b/c docker will change the name of the container
// b/c it's using a different image name, usually appending a _#.
return errors.Errorf("mismatched image prefix '%s' != '%s'", target.ImagePrefix, source.ImagePrefix)
}
for _, project := range source.Projects {
if _, exists := target.Projects[project.Name]; exists {
return errors.Errorf("duplicate project with name %s found in %s", project.Name, source.Filename)
}
}
for _, project := range source.Projects {
target.Projects[project.Name] = project
}
for name := range source.Networks {
if _, ok := target.Networks[name]; ok {
return errors.Errorf("duplicate network with name %s found in %s", name, source.Filename)
}
}
for name, network := range source.Networks {
target.Networks[name] = network
}
for name := range source.Registries {
if _, ok := target.Registries[name]; ok {
return errors.Errorf("duplicate registry with name %s found in %s", name, source.Filename)
}
}
for name, registry := range source.Registries {
target.Registries[name] = registry
}
return nil
}