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>")
86
// Check that the supplied cloud is valid.
88
if _, err := c.cloudByNameFunc(c.Cloud); err != nil {
89
if errors.IsNotFound(err) {
90
return errors.NotValidf("cloud %v", c.Cloud)
95
return errors.New("Usage: juju add-credential <cloud-name> [-f <credentials.yaml>]")
94
98
return cmd.CheckEmpty(args[1:])
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) {
106
builtInProviders := builtInProviders()
107
if builtIn, ok := builtInProviders[cloudName]; !ok {
108
return nil, errors.NotValidf("cloud %v", cloudName)
97
116
func (c *addCredentialCommand) Run(ctxt *cmd.Context) error {
117
// Check that the supplied cloud is valid.
119
if c.cloud, err = cloudOrProvider(c.CloudName, c.cloudByNameFunc); err != nil {
120
if !errors.IsNotFound(err) {
124
if len(c.cloud.AuthTypes) == 0 {
125
return errors.Errorf("cloud %q does not require credentials", c.CloudName)
128
if c.CredentialsFile == "" {
129
credentialsProvider, err := environs.Provider(c.cloud.Type)
131
return errors.Annotate(err, "getting provider for cloud")
133
return c.interactiveAddCredential(ctxt, credentialsProvider.CredentialSchemas())
98
135
data, err := ioutil.ReadFile(c.CredentialsFile)
100
137
return errors.Annotate(err, "reading credentials file")
105
142
return errors.Annotate(err, "parsing credentials file")
107
credentials, ok := specifiedCredentials[c.Cloud]
144
credentials, ok := specifiedCredentials[c.CloudName]
109
return errors.Errorf("no credentials for cloud %s exist in file %s", c.Cloud, c.CredentialsFile)
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)
148
existingCredentials, err := c.existingCredentialsForCloud()
150
return errors.Trace(err)
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)
156
for name, cred := range credentials.AuthCredentials {
157
existingCredentials.AuthCredentials[name] = cred
159
err = c.store.UpdateCredential(c.CloudName, *existingCredentials)
163
fmt.Fprintf(ctxt.Stdout, "credentials updated for cloud %s\n", c.CloudName)
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")
115
172
if errors.IsNotFound(err) {
116
173
existingCredentials = &jujucloud.CloudCredential{
117
174
AuthCredentials: make(map[string]jujucloud.Credential),
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)
124
for name, cred := range credentials.AuthCredentials {
125
existingCredentials.AuthCredentials[name] = cred
127
err = c.store.UpdateCredential(c.Cloud, *existingCredentials)
131
fmt.Fprintf(ctxt.Stdout, "credentials updated for cloud %s\n", c.Cloud)
177
return existingCredentials, nil
180
func (c *addCredentialCommand) interactiveAddCredential(ctxt *cmd.Context, schemas map[jujucloud.AuthType]jujucloud.CredentialSchema) error {
182
credentialName, err := c.promptCredentialName(ctxt.Stderr, ctxt.Stdin)
184
return errors.Trace(err)
186
if credentialName == "" {
187
fmt.Fprintln(ctxt.Stderr, "credentials entry aborted")
191
// Prompt to overwrite if needed.
192
existingCredentials, err := c.existingCredentialsForCloud()
194
return errors.Trace(err)
196
if _, ok := existingCredentials.AuthCredentials[credentialName]; ok {
197
overwrite, err := c.promptReplace(ctxt.Stderr, ctxt.Stdin)
199
return errors.Trace(err)
206
authType, err := c.promptAuthType(ctxt.Stderr, ctxt.Stdin, c.cloud.AuthTypes)
208
return errors.Trace(err)
210
schema, ok := schemas[authType]
212
return errors.NotSupportedf("auth type %q for cloud %q", authType, c.CloudName)
215
attrs, err := c.promptCredentialAttributes(ctxt, ctxt.Stderr, ctxt.Stdin, authType, schema)
217
return errors.Trace(err)
219
newCredential := jujucloud.NewCredential(authType, attrs)
220
existingCredentials.AuthCredentials[credentialName] = newCredential
221
err = c.store.UpdateCredential(c.CloudName, *existingCredentials)
223
return errors.Trace(err)
225
fmt.Fprintf(ctxt.Stdout, "credentials added for cloud %s\n\n", c.CloudName)
229
func (c *addCredentialCommand) promptCredentialName(out io.Writer, in io.Reader) (string, error) {
230
fmt.Fprint(out, " credential name: ")
231
input, err := readLine(in)
233
return "", errors.Trace(err)
235
return strings.TrimSpace(input), nil
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)
242
return false, errors.Trace(err)
244
return strings.ToLower(strings.TrimSpace(input)) == "y", nil
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
253
choices := make([]string, len(authTypes))
254
for i, a := range authTypes {
255
choices[i] = string(a)
261
fmt.Fprintf(out, " select auth-type [%v]: ", strings.Join(choices, ", "))
262
input, err := readLine(in)
264
return "", errors.Trace(err)
266
authType = strings.ToLower(strings.TrimSpace(input))
268
authType = string(authTypes[0])
271
for _, a := range authTypes {
272
if string(a) == authType {
280
fmt.Fprintf(out, " ...invalid auth type %q\n", authType)
282
return jujucloud.AuthType(authType), nil
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) {
289
attrs := make(map[string]string)
290
for _, attr := range schema {
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)
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 {
309
for _, choice := range currentAttr.Options {
310
if choice == value || value == "" {
316
fmt.Fprintf(out, " ...invalid value %q\n", value)
319
if value == "" && !currentAttr.Optional {
320
value = fmt.Sprintf("%v", currentAttr.Options[0])
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)
338
// Validate any file attribute is a valid file.
339
if value != "" && currentAttr.FilePath {
340
value, err = jujucloud.ValidateFileAttrValue(value)
342
fmt.Fprintf(out, " ...%s\n", err.Error())
347
// Stay in the loop if we need a mandatory value.
348
if value != "" || currentAttr.Optional {
353
attrs[currentAttr.Name] = value
359
func (c *addCredentialCommand) promptFieldValue(
360
out io.Writer, in io.Reader, attr jujucloud.NamedCredentialAttr,
364
// Formulate the prompt for the list of valid options.
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)
374
optionsPrompt = fmt.Sprintf(" [%v]", strings.Join(options, ","))
377
// Prompt for and accept input for field value.
378
fmt.Fprintf(out, " %s%s: ", name, optionsPrompt)
382
input, err = c.readHiddenField(in)
385
input, err = readLine(in)
388
return "", errors.Trace(err)
390
value := strings.TrimSpace(input)
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()))
398
return "", errors.Trace(err)
400
return string(value), nil