/
project_create_vcs.go
342 lines (313 loc) · 9.1 KB
/
project_create_vcs.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
338
339
340
341
342
package commands
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/Masterminds/goutils"
survey "gopkg.in/AlecAivazis/survey.v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/yaml"
"github.com/brigadecore/brigade/pkg/brigade"
"github.com/brigadecore/brigade/pkg/storage"
"github.com/brigadecore/brigade/pkg/storage/kube"
)
// projectCreatePromptsVCS handles all of the prompts.
//
// Default values are read from the given project. Values are then
// replaced on that object.
func projectCreatePromptsVCS(p *brigade.Project, store storage.Store) error {
err := setProjectName(p, store, true)
if err != nil {
return fmt.Errorf(abort, err)
}
// a couple of questions that make sense only if the Project is VCS-backed
qs := []*survey.Question{
{
Name: "name",
Prompt: &survey.Input{
Message: "Full repository name",
Help: "A protocol-neutral path to your repo, like github.com/foo/bar",
Default: p.Repo.Name,
},
},
{
Name: "cloneURL",
Prompt: &survey.Input{
Message: "Clone URL (https://github.com/your/repo.git)",
Help: "The URL that Git should use to clone. The protocol (https, git, ssh) will determine how the repo is fetched.",
Default: p.Repo.CloneURL,
},
},
}
if err = survey.Ask(qs, &p.Repo); err != nil {
return fmt.Errorf(abort, err)
}
// Don't prompt for key if the URL is HTTP(S).
if !isHTTP(p.Repo.CloneURL) {
var fname string
err := survey.AskOne(&survey.Input{
Message: "Path to SSH key for SSH clone URLs (leave blank to skip)",
Help: "The local path to an SSH key file, which will be uploaded to the project. Use this for SSH clone URLs.",
}, &fname, loadFileValidator)
if err != nil {
return fmt.Errorf(abort, err)
}
if key := loadFileStr(fname); key != "" {
p.Repo.SSHKey = replaceNewlines(key)
}
}
err = addEditSecrets(p, store)
if err != nil {
return err
}
if p.SharedSecret == "" {
const (
autogenerated = "Auto-generate one now"
userSpecified = "Specify my own"
leftUndefined = "Leave undefined"
)
var sharedSecretQuestionResult string
if err := survey.Ask(
[]*survey.Question{
{
Name: "sharedSecretSource",
Prompt: &survey.Select{
Message: "Where should the project's shared secret come from?",
Help: "Specifying your own shared secret is a useful option if " +
"your VCS gateway\nserves webhooks for multiple Brigade " +
"projects. (This is common.)" +
"\n\n" +
"Leaving the shared secret undefined is an advanced option " +
"that also addresses\nthe case of a single VCS gateway serving " +
"webhooks for multiple Brigade projects.\nVCS gateways that " +
"are able to do so AND are configured appropriately will " +
"fall\nback on a globally-defined default shared secret.",
Options: []string{
autogenerated,
userSpecified,
leftUndefined,
},
},
},
},
&sharedSecretQuestionResult,
); err != nil {
return fmt.Errorf(abort, err)
}
switch sharedSecretQuestionResult {
case autogenerated:
p.SharedSecret, _ = goutils.RandomAlphaNumeric(24)
fmt.Printf("Auto-generated a Shared Secret: %q\n", p.SharedSecret)
case userSpecified:
if err := survey.Ask(
[]*survey.Question{
{
Name: "userSpecifiedSharedSecret",
Prompt: &survey.Input{
Message: "Shared Secret",
},
},
},
&p.SharedSecret,
); err != nil {
return fmt.Errorf(abort, err)
}
case leftUndefined: // Nothing to do
}
}
configureGitHub := false
if err := survey.AskOne(&survey.Confirm{
Message: "Configure GitHub Access?",
Help: "Configure GitHub CI/CD integration for this project",
}, &configureGitHub, nil); err != nil {
return fmt.Errorf(abort, err)
} else if configureGitHub {
if err := survey.Ask([]*survey.Question{
{
Name: "token",
Prompt: &survey.Input{
Message: "OAuth2 token",
Help: "Used for contacting the GitHub API. GitHub issues this.",
Default: p.Github.Token,
},
},
{
Name: "baseURL",
Prompt: &survey.Input{
Message: "GitHub Enterprise URL",
Help: "If using GitHub Enterprise, set the base URL here",
Default: p.Github.BaseURL,
},
},
{
Name: "uploadURL",
Prompt: &survey.Input{
Message: "GitHub Enterprise upload URL",
Help: "If using GitHub Enterprise, set the upload URL here",
Default: p.Github.UploadURL,
},
},
}, &p.Github); err != nil {
return fmt.Errorf(abort, err)
}
}
doAdvanced := false
if err := survey.AskOne(&survey.Confirm{
Message: "Configure advanced options",
Help: "Show the advanced configuration options for projects",
}, &doAdvanced, nil); err != nil {
return fmt.Errorf(abort, err)
} else if doAdvanced {
return projectAdvancedPromptsVCS(p, store)
}
return nil
}
func projectAdvancedPromptsVCS(p *brigade.Project, store storage.Store) error {
questionsKubernetes, err := advancedQuestionsKubernetes(p, store)
if err != nil {
return fmt.Errorf(abort, err)
}
// VCSSidecar question makes sense only when we use a VCS
questionsKubernetes = append(questionsKubernetes, &survey.Question{
Name: "vCSSidecar",
Prompt: &survey.Input{
Message: "Custom VCS sidecar (enter 'NONE' for no sidecar)",
Help: "The default sidecar uses Git to fetch your repository (enter 'NONE' for no sidecar)",
Default: p.Kubernetes.VCSSidecar,
},
})
if err := survey.Ask(questionsKubernetes, &p.Kubernetes); err != nil {
return fmt.Errorf(abort, err)
}
if p.Kubernetes.BuildStorageClass == leftUndefined {
p.Kubernetes.BuildStorageClass = ""
}
if p.Kubernetes.CacheStorageClass == leftUndefined {
p.Kubernetes.CacheStorageClass = ""
}
questionsWorker := advancedQuestionsWorker(p, store)
if err := survey.Ask(questionsWorker, &p.Worker); err != nil {
return fmt.Errorf(abort, err)
}
questionsProject := advancedQuestionsProject(p, store)
// adding a couple of questions that make sense only when we do have a VCS
questionsProject = append(questionsProject, []*survey.Question{
{
Name: "initGitSubmodules",
Prompt: &survey.Confirm{
Message: "Initialize Git submodules",
Help: "For repos that have submodules, initialize them on each clone. Not recommended on public repos.",
Default: p.InitGitSubmodules,
},
},
{
Name: "brigadejsPath",
Prompt: &survey.Input{
Message: "brigade.js file path relative to the repository root",
Help: "brigade.js file path relative to the repository root, e.g. 'mypath/brigade.js'",
Default: p.BrigadejsPath,
},
Validate: func(ans interface{}) error {
sans := fmt.Sprintf("%v", ans)
if filepath.IsAbs(sans) {
return errors.New("Path must be relative")
}
return nil
},
},
{
Name: "brigadeConfigPath",
Prompt: &survey.Input{
Message: "brigade.json file path relative to the repository root",
Help: "brigade.json file path relative to the repository root, e.g. 'mypath/brigade.json'",
Default: p.BrigadeConfigPath,
},
Validate: func(ans interface{}) error {
sans := fmt.Sprintf("%v", ans)
if filepath.IsAbs(sans) {
return errors.New("Path must be relative")
}
return nil
},
},
}...)
if err := survey.Ask(questionsProject, p); err != nil {
return fmt.Errorf(abort, err)
}
err = addBrigadeJS(p, store)
if err != nil {
return fmt.Errorf(abort, err)
}
err = addBrigadeConfig(p, store)
if err != nil {
return fmt.Errorf(abort, err)
}
err = addGenericGatewaySecret(p, store)
if err != nil {
return fmt.Errorf(abort, err)
}
return nil
}
// loadFileValidator validates that a file exists and can be read.
func loadFileValidator(val interface{}) error {
name := os.ExpandEnv(val.(string))
if name == "" {
return nil
}
_, err := ioutil.ReadFile(name)
return err
}
// loadFileStr should not be called unless loadFileValidator is called first.
func loadFileStr(name string) string {
if name == "" {
return ""
}
data, err := ioutil.ReadFile(name)
if err != nil {
return ""
}
return string(data)
}
func replaceNewlines(data string) string {
return strings.Replace(data, "\n", "$", -1)
}
// loadProjectConfig loads a project configuration from the local filesystem.
func loadProjectConfig(file string, proj *brigade.Project) (*brigade.Project, error) {
rdr, err := os.Open(file)
if err != nil {
return proj, err
}
defer rdr.Close()
sec, err := parseSecret(rdr)
if err != nil {
return proj, err
}
if sec.Name == "" {
return proj, fmt.Errorf("secret in %s is missing required name field", file)
}
return kube.NewProjectFromSecret(sec, "")
}
func parseSecret(reader io.Reader) (*v1.Secret, error) {
dec := yaml.NewYAMLOrJSONDecoder(reader, 4096)
secret := &v1.Secret{}
// We are only decoding the first item in the YAML.
err := dec.Decode(secret)
// Convert StringData to Data
if len(secret.StringData) > 0 {
if secret.Data == nil {
secret.Data = map[string][]byte{}
}
for key, val := range secret.StringData {
secret.Data[key] = []byte(val)
}
}
return secret, err
}
func isHTTP(str string) bool {
str = strings.ToLower(str)
return strings.HasPrefix(str, "http:") || strings.HasPrefix(str, "https:")
}