~juju-qa/ubuntu/xenial/juju/xenial-2.0-beta3

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/cmd/juju/cloud/addcredential.go

  • Committer: Martin Packman
  • Date: 2016-03-30 19:31:08 UTC
  • mfrom: (1.1.41)
  • Revision ID: martin.packman@canonical.com-20160330193108-h9iz3ak334uk0z5r
Merge new upstream source 2.0~beta3

Show diffs side-by-side

added added

removed removed

Lines of Context:
5
5
 
6
6
import (
7
7
        "fmt"
 
8
        "io"
8
9
        "io/ioutil"
 
10
        "os"
 
11
        "strings"
9
12
 
10
13
        "github.com/juju/cmd"
11
14
        "github.com/juju/errors"
 
15
        "golang.org/x/crypto/ssh/terminal"
12
16
        "launchpad.net/gnuflag"
13
17
 
14
18
        jujucloud "github.com/juju/juju/cloud"
 
19
        "github.com/juju/juju/environs"
15
20
        "github.com/juju/juju/jujuclient"
16
21
)
17
22
 
23
28
        // Replace, if true, existing credential information is overwritten.
24
29
        Replace bool
25
30
 
26
 
        // Cloud is the name of the cloud for which we add credentials.
27
 
        Cloud string
 
31
        // CloudName is the name of the cloud for which we add credentials.
 
32
        CloudName string
28
33
 
29
34
        // CredentialsFile is the name of the credentials YAML file.
30
35
        CredentialsFile string
 
36
 
 
37
        cloud *jujucloud.Cloud
31
38
}
32
39
 
33
40
var addCredentialDoc = `
34
41
The add-credential command adds or replaces credentials for a given cloud.
35
42
 
36
43
The user is required to specify the name of the cloud for which credentials
37
 
will be added/replaced, and a YAML file containing credentials.
 
44
will be added/replaced, and optionally a YAML file containing credentials.
38
45
A sample YAML snippet is:
39
46
 
40
47
credentials:
49
56
option is required to overwite. Note that any default region which may have
50
57
been defined is never overwritten.
51
58
 
 
59
If no YAML file is specified, the user is prompted to add credentials interactively.
 
60
 
52
61
Example:
 
62
   juju add-credential aws
53
63
   juju add-credential aws -f my-credentials.yaml
54
64
   juju add-credential aws -f my-credentials.yaml --replace
55
65
 
56
66
See Also:
57
67
   juju list-credentials
 
68
   juju remove-credential
58
69
`
59
70
 
60
71
// NewAddCredentialCommand returns a command to add credential information.
80
91
}
81
92
 
82
93
func (c *addCredentialCommand) Init(args []string) (err error) {
83
 
        if len(args) < 1 || c.CredentialsFile == "" {
84
 
                return errors.New("Usage: juju add-credential <cloud-name> -f <credentials.yaml>")
85
 
        }
86
 
        // Check that the supplied cloud is valid.
87
 
        c.Cloud = args[0]
88
 
        if _, err := c.cloudByNameFunc(c.Cloud); err != nil {
89
 
                if errors.IsNotFound(err) {
90
 
                        return errors.NotValidf("cloud %v", c.Cloud)
91
 
                }
92
 
                return err
93
 
        }
 
94
        if len(args) < 1 {
 
95
                return errors.New("Usage: juju add-credential <cloud-name> [-f <credentials.yaml>]")
 
96
        }
 
97
        c.CloudName = args[0]
94
98
        return cmd.CheckEmpty(args[1:])
95
99
}
96
100
 
 
101
func cloudOrProvider(cloudName string, cloudByNameFunc func(string) (*jujucloud.Cloud, error)) (cloud *jujucloud.Cloud, err error) {
 
102
        if cloud, err = cloudByNameFunc(cloudName); err != nil {
 
103
                if !errors.IsNotFound(err) {
 
104
                        return nil, err
 
105
                }
 
106
                builtInProviders := builtInProviders()
 
107
                if builtIn, ok := builtInProviders[cloudName]; !ok {
 
108
                        return nil, errors.NotValidf("cloud %v", cloudName)
 
109
                } else {
 
110
                        cloud = &builtIn
 
111
                }
 
112
        }
 
113
        return cloud, nil
 
114
}
 
115
 
97
116
func (c *addCredentialCommand) Run(ctxt *cmd.Context) error {
 
117
        // Check that the supplied cloud is valid.
 
118
        var err error
 
119
        if c.cloud, err = cloudOrProvider(c.CloudName, c.cloudByNameFunc); err != nil {
 
120
                if !errors.IsNotFound(err) {
 
121
                        return err
 
122
                }
 
123
        }
 
124
        if len(c.cloud.AuthTypes) == 0 {
 
125
                return errors.Errorf("cloud %q does not require credentials", c.CloudName)
 
126
        }
 
127
 
 
128
        if c.CredentialsFile == "" {
 
129
                credentialsProvider, err := environs.Provider(c.cloud.Type)
 
130
                if err != nil {
 
131
                        return errors.Annotate(err, "getting provider for cloud")
 
132
                }
 
133
                return c.interactiveAddCredential(ctxt, credentialsProvider.CredentialSchemas())
 
134
        }
98
135
        data, err := ioutil.ReadFile(c.CredentialsFile)
99
136
        if err != nil {
100
137
                return errors.Annotate(err, "reading credentials file")
104
141
        if err != nil {
105
142
                return errors.Annotate(err, "parsing credentials file")
106
143
        }
107
 
        credentials, ok := specifiedCredentials[c.Cloud]
 
144
        credentials, ok := specifiedCredentials[c.CloudName]
108
145
        if !ok {
109
 
                return errors.Errorf("no credentials for cloud %s exist in file %s", c.Cloud, c.CredentialsFile)
110
 
        }
111
 
        existingCredentials, err := c.store.CredentialForCloud(c.Cloud)
 
146
                return errors.Errorf("no credentials for cloud %s exist in file %s", c.CloudName, c.CredentialsFile)
 
147
        }
 
148
        existingCredentials, err := c.existingCredentialsForCloud()
 
149
        if err != nil {
 
150
                return errors.Trace(err)
 
151
        }
 
152
        // If there are *any* credentials already for the cloud, we'll ask for the --replace flag.
 
153
        if !c.Replace && len(existingCredentials.AuthCredentials) > 0 && len(credentials.AuthCredentials) > 0 {
 
154
                return errors.Errorf("credentials for cloud %s already exist; use --replace to overwrite / merge", c.CloudName)
 
155
        }
 
156
        for name, cred := range credentials.AuthCredentials {
 
157
                existingCredentials.AuthCredentials[name] = cred
 
158
        }
 
159
        err = c.store.UpdateCredential(c.CloudName, *existingCredentials)
 
160
        if err != nil {
 
161
                return err
 
162
        }
 
163
        fmt.Fprintf(ctxt.Stdout, "credentials updated for cloud %s\n", c.CloudName)
 
164
        return nil
 
165
}
 
166
 
 
167
func (c *addCredentialCommand) existingCredentialsForCloud() (*jujucloud.CloudCredential, error) {
 
168
        existingCredentials, err := c.store.CredentialForCloud(c.CloudName)
112
169
        if err != nil && !errors.IsNotFound(err) {
113
 
                return errors.Annotate(err, "reading existing credentials for cloud")
 
170
                return nil, errors.Annotate(err, "reading existing credentials for cloud")
114
171
        }
115
172
        if errors.IsNotFound(err) {
116
173
                existingCredentials = &jujucloud.CloudCredential{
117
174
                        AuthCredentials: make(map[string]jujucloud.Credential),
118
175
                }
119
176
        }
120
 
        // If there are *any* credentials already for the cloud, we'll ask for the --replace flag.
121
 
        if !c.Replace && len(existingCredentials.AuthCredentials) > 0 && len(credentials.AuthCredentials) > 0 {
122
 
                return errors.Errorf("credentials for cloud %s already exist; use --replace to overwrite / merge", c.Cloud)
123
 
        }
124
 
        for name, cred := range credentials.AuthCredentials {
125
 
                existingCredentials.AuthCredentials[name] = cred
126
 
        }
127
 
        err = c.store.UpdateCredential(c.Cloud, *existingCredentials)
128
 
        if err != nil {
129
 
                return err
130
 
        }
131
 
        fmt.Fprintf(ctxt.Stdout, "credentials updated for cloud %s\n", c.Cloud)
 
177
        return existingCredentials, nil
 
178
}
 
179
 
 
180
func (c *addCredentialCommand) interactiveAddCredential(ctxt *cmd.Context, schemas map[jujucloud.AuthType]jujucloud.CredentialSchema) error {
 
181
        var err error
 
182
        credentialName, err := c.promptCredentialName(ctxt.Stderr, ctxt.Stdin)
 
183
        if err != nil {
 
184
                return errors.Trace(err)
 
185
        }
 
186
        if credentialName == "" {
 
187
                fmt.Fprintln(ctxt.Stderr, "credentials entry aborted")
 
188
                return nil
 
189
        }
 
190
 
 
191
        // Prompt to overwrite if needed.
 
192
        existingCredentials, err := c.existingCredentialsForCloud()
 
193
        if err != nil {
 
194
                return errors.Trace(err)
 
195
        }
 
196
        if _, ok := existingCredentials.AuthCredentials[credentialName]; ok {
 
197
                overwrite, err := c.promptReplace(ctxt.Stderr, ctxt.Stdin)
 
198
                if err != nil {
 
199
                        return errors.Trace(err)
 
200
                }
 
201
                if !overwrite {
 
202
                        return nil
 
203
                }
 
204
        }
 
205
 
 
206
        authType, err := c.promptAuthType(ctxt.Stderr, ctxt.Stdin, c.cloud.AuthTypes)
 
207
        if err != nil {
 
208
                return errors.Trace(err)
 
209
        }
 
210
        schema, ok := schemas[authType]
 
211
        if !ok {
 
212
                return errors.NotSupportedf("auth type %q for cloud %q", authType, c.CloudName)
 
213
        }
 
214
 
 
215
        attrs, err := c.promptCredentialAttributes(ctxt, ctxt.Stderr, ctxt.Stdin, authType, schema)
 
216
        if err != nil {
 
217
                return errors.Trace(err)
 
218
        }
 
219
        newCredential := jujucloud.NewCredential(authType, attrs)
 
220
        existingCredentials.AuthCredentials[credentialName] = newCredential
 
221
        err = c.store.UpdateCredential(c.CloudName, *existingCredentials)
 
222
        if err != nil {
 
223
                return errors.Trace(err)
 
224
        }
 
225
        fmt.Fprintf(ctxt.Stdout, "credentials added for cloud %s\n\n", c.CloudName)
132
226
        return nil
133
227
}
 
228
 
 
229
func (c *addCredentialCommand) promptCredentialName(out io.Writer, in io.Reader) (string, error) {
 
230
        fmt.Fprint(out, "  credential name: ")
 
231
        input, err := readLine(in)
 
232
        if err != nil {
 
233
                return "", errors.Trace(err)
 
234
        }
 
235
        return strings.TrimSpace(input), nil
 
236
}
 
237
 
 
238
func (c *addCredentialCommand) promptReplace(out io.Writer, in io.Reader) (bool, error) {
 
239
        fmt.Fprint(out, "  replace existing credential? [y/N]: ")
 
240
        input, err := readLine(in)
 
241
        if err != nil {
 
242
                return false, errors.Trace(err)
 
243
        }
 
244
        return strings.ToLower(strings.TrimSpace(input)) == "y", nil
 
245
}
 
246
 
 
247
func (c *addCredentialCommand) promptAuthType(out io.Writer, in io.Reader, authTypes []jujucloud.AuthType) (jujucloud.AuthType, error) {
 
248
        if len(authTypes) == 1 {
 
249
                fmt.Fprintf(out, "  auth-type: %v\n", authTypes[0])
 
250
                return authTypes[0], nil
 
251
        }
 
252
        authType := ""
 
253
        choices := make([]string, len(authTypes))
 
254
        for i, a := range authTypes {
 
255
                choices[i] = string(a)
 
256
                if i == 0 {
 
257
                        choices[i] += "*"
 
258
                }
 
259
        }
 
260
        for {
 
261
                fmt.Fprintf(out, "  select auth-type [%v]: ", strings.Join(choices, ", "))
 
262
                input, err := readLine(in)
 
263
                if err != nil {
 
264
                        return "", errors.Trace(err)
 
265
                }
 
266
                authType = strings.ToLower(strings.TrimSpace(input))
 
267
                if authType == "" {
 
268
                        authType = string(authTypes[0])
 
269
                }
 
270
                isValid := false
 
271
                for _, a := range authTypes {
 
272
                        if string(a) == authType {
 
273
                                isValid = true
 
274
                                break
 
275
                        }
 
276
                }
 
277
                if isValid {
 
278
                        break
 
279
                }
 
280
                fmt.Fprintf(out, "  ...invalid auth type %q\n", authType)
 
281
        }
 
282
        return jujucloud.AuthType(authType), nil
 
283
}
 
284
 
 
285
func (c *addCredentialCommand) promptCredentialAttributes(
 
286
        ctxt *cmd.Context, out io.Writer, in io.Reader, authType jujucloud.AuthType, schema jujucloud.CredentialSchema,
 
287
) (map[string]string, error) {
 
288
 
 
289
        attrs := make(map[string]string)
 
290
        for _, attr := range schema {
 
291
                currentAttr := attr
 
292
                value := ""
 
293
                for {
 
294
                        var err error
 
295
                        // Interactive add does not support adding multi-line values, which
 
296
                        // is what we typically get when the attribute can come from a file.
 
297
                        // For now we'll skip, and just get the user to enter the file path.
 
298
                        // TODO(wallyworld) - add support for multi-line entry
 
299
                        if currentAttr.FileAttr == "" {
 
300
                                value, err = c.promptFieldValue(out, in, currentAttr)
 
301
                                if err != nil {
 
302
                                        return nil, err
 
303
                                }
 
304
                        }
 
305
                        // Validate the entered value matches any options.
 
306
                        // If the user just hits Enter, the first option is used.
 
307
                        if len(currentAttr.Options) > 0 {
 
308
                                isValid := false
 
309
                                for _, choice := range currentAttr.Options {
 
310
                                        if choice == value || value == "" {
 
311
                                                isValid = true
 
312
                                                break
 
313
                                        }
 
314
                                }
 
315
                                if !isValid {
 
316
                                        fmt.Fprintf(out, "  ...invalid value %q\n", value)
 
317
                                        continue
 
318
                                }
 
319
                                if value == "" && !currentAttr.Optional {
 
320
                                        value = fmt.Sprintf("%v", currentAttr.Options[0])
 
321
                                }
 
322
                        }
 
323
 
 
324
                        // If the entered value is empty and the attribute can come
 
325
                        // from a file, prompt for that.
 
326
                        if value == "" && currentAttr.FileAttr != "" {
 
327
                                fileAttr := currentAttr
 
328
                                fileAttr.Name = currentAttr.FileAttr
 
329
                                fileAttr.Hidden = false
 
330
                                fileAttr.FilePath = true
 
331
                                currentAttr = fileAttr
 
332
                                value, err = c.promptFieldValue(out, in, currentAttr)
 
333
                                if err != nil {
 
334
                                        return nil, err
 
335
                                }
 
336
                        }
 
337
 
 
338
                        // Validate any file attribute is a valid file.
 
339
                        if value != "" && currentAttr.FilePath {
 
340
                                value, err = jujucloud.ValidateFileAttrValue(value)
 
341
                                if err != nil {
 
342
                                        fmt.Fprintf(out, "  ...%s\n", err.Error())
 
343
                                        continue
 
344
                                }
 
345
                        }
 
346
 
 
347
                        // Stay in the loop if we need a mandatory value.
 
348
                        if value != "" || currentAttr.Optional {
 
349
                                break
 
350
                        }
 
351
                }
 
352
                if value != "" {
 
353
                        attrs[currentAttr.Name] = value
 
354
                }
 
355
        }
 
356
        return attrs, nil
 
357
}
 
358
 
 
359
func (c *addCredentialCommand) promptFieldValue(
 
360
        out io.Writer, in io.Reader, attr jujucloud.NamedCredentialAttr,
 
361
) (string, error) {
 
362
 
 
363
        name := attr.Name
 
364
        // Formulate the prompt for the list of valid options.
 
365
        optionsPrompt := ""
 
366
        if len(attr.Options) > 0 {
 
367
                options := make([]string, len(attr.Options))
 
368
                for i, opt := range attr.Options {
 
369
                        options[i] = fmt.Sprintf("%v", opt)
 
370
                        if i == 0 {
 
371
                                options[i] += "*"
 
372
                        }
 
373
                }
 
374
                optionsPrompt = fmt.Sprintf(" [%v]", strings.Join(options, ","))
 
375
        }
 
376
 
 
377
        // Prompt for and accept input for field value.
 
378
        fmt.Fprintf(out, "  %s%s: ", name, optionsPrompt)
 
379
        var input string
 
380
        var err error
 
381
        if attr.Hidden {
 
382
                input, err = c.readHiddenField(in)
 
383
                fmt.Fprintln(out)
 
384
        } else {
 
385
                input, err = readLine(in)
 
386
        }
 
387
        if err != nil {
 
388
                return "", errors.Trace(err)
 
389
        }
 
390
        value := strings.TrimSpace(input)
 
391
        return value, nil
 
392
}
 
393
 
 
394
func (c *addCredentialCommand) readHiddenField(in io.Reader) (string, error) {
 
395
        if f, ok := in.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) {
 
396
                value, err := terminal.ReadPassword(int(f.Fd()))
 
397
                if err != nil {
 
398
                        return "", errors.Trace(err)
 
399
                }
 
400
                return string(value), nil
 
401
        }
 
402
        return readLine(in)
 
403
}