-
Notifications
You must be signed in to change notification settings - Fork 12
/
config.go
295 lines (257 loc) · 9.77 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
package config
import (
"fmt"
"io/ioutil"
"net/url"
"path"
"strings"
"github.com/gruntwork-io/go-commons/version"
goversion "github.com/hashicorp/go-version"
"gopkg.in/yaml.v2"
"github.com/gruntwork-io/boilerplate/errors"
"github.com/gruntwork-io/boilerplate/options"
"github.com/gruntwork-io/boilerplate/util"
"github.com/gruntwork-io/boilerplate/variables"
)
const BOILERPLATE_CONFIG_FILE = "boilerplate.yml"
// The contents of a boilerplate.yml config file
type BoilerplateConfig struct {
RequiredVersion *string
Variables []variables.Variable
Dependencies []variables.Dependency
Hooks variables.Hooks
Partials []string
SkipFiles []variables.SkipFile
Engines []variables.Engine
}
// GetVariablesMap returns a map that maps variable names to the variable config.
func (config *BoilerplateConfig) GetVariablesMap() map[string]variables.Variable {
out := make(map[string]variables.Variable)
for _, variable := range config.Variables {
out[variable.Name()] = variable
}
return out
}
// Implement the go-yaml unmarshal interface for BoilerplateConfig. We can't let go-yaml handle this itself because:
//
// 1. Variable is an interface
// 2. We need to provide Defaults for optional fields, such as "type"
// 3. We want to validate the variable as part of the unmarshalling process so we never have invalid Variable or
// Dependency classes floating around
func (config *BoilerplateConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
var fields map[string]interface{}
if err := unmarshal(&fields); err != nil {
return err
}
requiredVersion, err := variables.UnmarshalString(fields, "required_version", false)
if err != nil {
return err
}
vars, err := variables.UnmarshalVariablesFromBoilerplateConfigYaml(fields)
if err != nil {
return err
}
deps, err := variables.UnmarshalDependenciesFromBoilerplateConfigYaml(fields)
if err != nil {
return err
}
hooks, err := variables.UnmarshalHooksFromBoilerplateConfigYaml(fields)
if err != nil {
return err
}
partials, err := variables.UnmarshalListOfStrings(fields, "partials")
if err != nil {
return err
}
skipFiles, err := variables.UnmarshalSkipFilesFromBoilerplateConfigYaml(fields)
if err != nil {
return err
}
engines, err := variables.UnmarshalEnginesFromBoilerplateConfigYaml(fields)
if err != nil {
return err
}
*config = BoilerplateConfig{
RequiredVersion: requiredVersion,
Variables: vars,
Dependencies: deps,
Hooks: hooks,
Partials: partials,
SkipFiles: skipFiles,
Engines: engines,
}
return nil
}
// Implement the go-yaml marshaler interface so that the config can be marshaled into yaml. We use a custom marshaler
// instead of defining the fields as tags so that we skip the attributes that are empty.
func (config *BoilerplateConfig) MarshalYAML() (interface{}, error) {
configYml := map[string]interface{}{}
if len(config.Variables) > 0 {
// Due to go type system, we can only pass through []interface{}, even though []Variable is technically
// polymorphic to that type. So we reconstruct the list using the right type before passing it in to the marshal
// function.
interfaceList := []interface{}{}
for _, variable := range config.Variables {
interfaceList = append(interfaceList, variable)
}
varsYml, err := util.MarshalListOfObjectsToYAML(interfaceList)
if err != nil {
return nil, err
}
configYml["variables"] = varsYml
}
if len(config.Dependencies) > 0 {
// Due to go type system, we can only pass through []interface{}, even though []Dependency is technically
// polymorphic to that type. So we reconstruct the list using the right type before passing it in to the marshal
// function.
interfaceList := []interface{}{}
for _, dep := range config.Dependencies {
interfaceList = append(interfaceList, dep)
}
depsYml, err := util.MarshalListOfObjectsToYAML(interfaceList)
if err != nil {
return nil, err
}
configYml["dependencies"] = depsYml
}
if len(config.Hooks.BeforeHooks) > 0 || len(config.Hooks.AfterHooks) > 0 {
hooksYml, err := config.Hooks.MarshalYAML()
if err != nil {
return nil, err
}
configYml["hooks"] = hooksYml
}
if len(config.Partials) > 0 {
configYml["partials"] = config.Partials
}
if len(config.SkipFiles) > 0 {
// Due to go type system, we can only pass through []interface{}, even though []SkipFile is technically
// polymorphic to that type. So we reconstruct the list using the right type before passing it in to the marshal
// function.
interfaceList := []interface{}{}
for _, skipFile := range config.SkipFiles {
interfaceList = append(interfaceList, skipFile)
}
skipFilesYml, err := util.MarshalListOfObjectsToYAML(interfaceList)
if err != nil {
return nil, err
}
configYml["skip_files"] = skipFilesYml
}
if len(config.Engines) > 0 {
// Due to go type system, we can only pass through []interface{}, even though []Engine is technically
// polymorphic to that type. So we reconstruct the list using the right type before passing it in to the marshal
// function.
interfaceList := []interface{}{}
for _, engine := range config.Engines {
interfaceList = append(interfaceList, engine)
}
enginesYml, err := util.MarshalListOfObjectsToYAML(interfaceList)
if err != nil {
return nil, err
}
configYml["engines"] = enginesYml
}
return configYml, nil
}
// Load the boilerplate.yml config contents for the folder specified in the given options
func LoadBoilerplateConfig(opts *options.BoilerplateOptions) (*BoilerplateConfig, error) {
configPath := BoilerplateConfigPath(opts.TemplateFolder)
if util.PathExists(configPath) {
util.Logger.Printf("Loading boilerplate config from %s", configPath)
bytes, err := ioutil.ReadFile(configPath)
if err != nil {
return nil, errors.WithStackTrace(err)
}
return ParseBoilerplateConfig(bytes)
} else if opts.OnMissingConfig == options.Ignore {
util.Logger.Printf("Warning: boilerplate config file not found at %s. The %s flag is set, so ignoring. Note that no variables will be available while generating.", configPath, options.OptMissingConfigAction)
return &BoilerplateConfig{}, nil
} else {
// If the template URL is similar to a git URL, surface in error message that there may be a misspelling/typo.
return nil, errors.WithStackTrace(BoilerplateConfigNotFound(configPath))
}
}
// Parse the given configContents as a boilerplate.yml config file
func ParseBoilerplateConfig(configContents []byte) (*BoilerplateConfig, error) {
boilerplateConfig := &BoilerplateConfig{}
if err := yaml.Unmarshal(configContents, boilerplateConfig); err != nil {
return nil, errors.WithStackTrace(err)
}
converted, err := variables.ConvertYAMLToStringMap(boilerplateConfig)
if err != nil {
return boilerplateConfig, err
}
boilerplateConfig, ok := converted.(*BoilerplateConfig)
if !ok {
return nil, variables.YAMLConversionErr{Key: converted}
}
return boilerplateConfig, nil
}
// Return the default path for a boilerplate.yml config file in the given folder
func BoilerplateConfigPath(templateFolder string) string {
return path.Join(templateFolder, BOILERPLATE_CONFIG_FILE)
}
// EnforceRequiredVersion enforces any required_version string that is configured on the boilerplate config by checking
// against the current version of the CLI.
func EnforceRequiredVersion(boilerplateConfig *BoilerplateConfig) error {
// Base case: if required_version is not set, then there is no version to enforce.
if boilerplateConfig == nil || boilerplateConfig.RequiredVersion == nil {
return nil
}
constraint := *boilerplateConfig.RequiredVersion
// Base case: if using a development version, then bypass required version check
currentVersion := version.GetVersion()
if currentVersion == "" {
return nil
}
// At this point there is a valid version that needs to be checked against the constraint
boilerplateVersion, err := goversion.NewVersion(currentVersion)
if err != nil {
return errors.WithStackTrace(err)
}
versionConstraint, err := goversion.NewConstraint(constraint)
if err != nil {
return errors.WithStackTrace(err)
}
if !versionConstraint.Check(boilerplateVersion) {
return errors.WithStackTrace(InvalidBoilerplateVersion{CurrentVersion: boilerplateVersion, VersionConstraints: versionConstraint})
}
return nil
}
// maybeGitURL uses heuristics to attempt to decide if the URL may be a github URL that is encoded incorrectly.
func maybeGitURL(templateURL string) bool {
potentialGitURLs := []string{
"github.com",
"gitlab.com",
"bitbucket.org",
}
for _, url := range potentialGitURLs {
if strings.Contains(templateURL, url) {
return true
}
}
// If the URL can be parsed and any non-file URL part is parsed out, return that this may be a git URL.
parsed, err := url.Parse(templateURL)
if err != nil {
return false
}
return parsed.Scheme != "" || parsed.Hostname() != "" || parsed.RawQuery != ""
}
// Custom error types
type BoilerplateConfigNotFound string
func (err BoilerplateConfigNotFound) Error() string {
errMsg := fmt.Sprintf("Could not find %s in %s and the %s flag is set to %s", BOILERPLATE_CONFIG_FILE, string(err), options.OptMissingConfigAction, options.Exit)
configPath := string(err)
if maybeGitURL(configPath) {
errMsg += ". Template URL looks like a git repo. Did you misspell the URL? Should be encoded as one of the following: `git::ssh://git@github.com/ORG/REPO`, `github.com/ORG/REPO`, `https://github.com/ORG/REPO`, or `git@github.com:ORG/REPO`."
}
return errMsg
}
type InvalidBoilerplateVersion struct {
CurrentVersion *goversion.Version
VersionConstraints goversion.Constraints
}
func (err InvalidBoilerplateVersion) Error() string {
return fmt.Sprintf("The currently installed version of Boilerplate (%s) is not compatible with the version constraint requiring (%s).", err.CurrentVersion.String(), err.VersionConstraints.String())
}