github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/cmd/juju/cloud/detectcredentials.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package cloud 5 6 import ( 7 "bufio" 8 "fmt" 9 "io" 10 "sort" 11 "strconv" 12 "strings" 13 14 "github.com/juju/cmd" 15 "github.com/juju/errors" 16 "github.com/juju/utils/set" 17 18 jujucloud "github.com/juju/juju/cloud" 19 "github.com/juju/juju/cmd/juju/common" 20 "github.com/juju/juju/environs" 21 "github.com/juju/juju/jujuclient" 22 ) 23 24 type detectCredentialsCommand struct { 25 cmd.CommandBase 26 out cmd.Output 27 28 store jujuclient.CredentialStore 29 30 // registeredProvidersFunc is set by tests to return all registered environ providers 31 registeredProvidersFunc func() []string 32 33 // allCloudsFunc is set by tests to return all public and personal clouds 34 allCloudsFunc func() (map[string]jujucloud.Cloud, error) 35 36 // cloudByNameFunc is set by tests to return a named cloud. 37 cloudByNameFunc func(string) (*jujucloud.Cloud, error) 38 } 39 40 const detectCredentialsSummary = `Attempts to automatically add or replace credentials for a cloud.` 41 42 var detectCredentialsDoc = ` 43 Well known locations for specific clouds are searched and any found 44 information is presented interactively to the user. 45 An alternative to this command is ` + "`juju add-credential`" + `. 46 Below are the cloud types for which credentials may be autoloaded, 47 including the locations searched. 48 49 EC2 50 Credentials and regions: 51 1. On Linux, $HOME/.aws/credentials and $HOME/.aws/config 52 2. Environment variables AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY 53 54 GCE 55 Credentials: 56 1. A JSON file whose path is specified by the 57 GOOGLE_APPLICATION_CREDENTIALS environment variable 58 2. On Linux, $HOME/.config/gcloud/application_default_credentials.json 59 Default region is specified by the CLOUDSDK_COMPUTE_REGION environment 60 variable. 61 3. On Windows, %APPDATA%\gcloud\application_default_credentials.json 62 63 OpenStack 64 Credentials: 65 1. On Linux, $HOME/.novarc 66 2. Environment variables OS_USERNAME, OS_PASSWORD, OS_TENANT_NAME, 67 OS_DOMAIN_NAME 68 69 Example: 70 juju autoload-credentials 71 72 See also: 73 list-credentials 74 remove-credential 75 set-default-credential 76 add-credential 77 `[1:] 78 79 // NewDetectCredentialsCommand returns a command to add credential information to credentials.yaml. 80 func NewDetectCredentialsCommand() cmd.Command { 81 c := &detectCredentialsCommand{ 82 store: jujuclient.NewFileCredentialStore(), 83 registeredProvidersFunc: environs.RegisteredProviders, 84 cloudByNameFunc: jujucloud.CloudByName, 85 } 86 c.allCloudsFunc = func() (map[string]jujucloud.Cloud, error) { 87 return c.allClouds() 88 } 89 return c 90 } 91 92 func (c *detectCredentialsCommand) Info() *cmd.Info { 93 return &cmd.Info{ 94 Name: "autoload-credentials", 95 Purpose: detectCredentialsSummary, 96 Doc: detectCredentialsDoc, 97 } 98 } 99 100 type discoveredCredential struct { 101 defaultCloudName string 102 cloudType string 103 region string 104 credentialName string 105 credential jujucloud.Credential 106 isNew bool 107 } 108 109 func (c *detectCredentialsCommand) allClouds() (map[string]jujucloud.Cloud, error) { 110 clouds, _, err := jujucloud.PublicCloudMetadata(jujucloud.JujuPublicCloudsPath()) 111 if err != nil { 112 return nil, err 113 } 114 personalClouds, err := jujucloud.PersonalCloudMetadata() 115 if err != nil { 116 return nil, err 117 } 118 for k, v := range personalClouds { 119 clouds[k] = v 120 } 121 return clouds, nil 122 } 123 124 func (c *detectCredentialsCommand) Run(ctxt *cmd.Context) error { 125 fmt.Fprintln(ctxt.Stderr, "\nLooking for cloud and credential information locally...") 126 127 clouds, err := c.allCloudsFunc() 128 if err != nil { 129 return errors.Trace(err) 130 } 131 132 // Let's ensure a consistent order. 133 var sortedCloudNames []string 134 for cloudName := range clouds { 135 sortedCloudNames = append(sortedCloudNames, cloudName) 136 } 137 sort.Strings(sortedCloudNames) 138 139 // The default cloud name for each provider type is the 140 // first cloud in the sorted list. 141 defaultCloudNames := make(map[string]string) 142 for _, cloudName := range sortedCloudNames { 143 cloud := clouds[cloudName] 144 if _, ok := defaultCloudNames[cloud.Type]; ok { 145 continue 146 } 147 defaultCloudNames[cloud.Type] = cloudName 148 } 149 150 providerNames := c.registeredProvidersFunc() 151 sort.Strings(providerNames) 152 153 var discovered []discoveredCredential 154 discoveredLabels := set.NewStrings() 155 for _, providerName := range providerNames { 156 provider, err := environs.Provider(providerName) 157 if err != nil { 158 // Should never happen but it will on go 1.2 159 // because lxd provider is not built. 160 logger.Errorf("provider %q not available on this platform", providerName) 161 continue 162 } 163 if detectCredentials, ok := provider.(environs.ProviderCredentials); ok { 164 detected, err := detectCredentials.DetectCredentials() 165 if err != nil && !errors.IsNotFound(err) { 166 logger.Errorf("could not detect credentials for provider %q: %v", providerName, err) 167 continue 168 } 169 if errors.IsNotFound(err) || len(detected.AuthCredentials) == 0 { 170 continue 171 } 172 173 // For each credential, construct meta info for which cloud it may pertain to etc. 174 for credName, newCred := range detected.AuthCredentials { 175 if credName == "" { 176 logger.Debugf("ignoring unnamed credential for provider %s", providerName) 177 continue 178 } 179 // Ignore empty credentials. 180 if newCred.AuthType() == jujucloud.EmptyAuthType { 181 continue 182 } 183 // Check that another provider hasn't loaded the same credential. 184 if discoveredLabels.Contains(newCred.Label) { 185 continue 186 } 187 discoveredLabels.Add(newCred.Label) 188 189 credInfo := discoveredCredential{ 190 cloudType: providerName, 191 credentialName: credName, 192 credential: newCred, 193 } 194 195 // Fill in the default cloud and other meta information. 196 defaultCloud, existingDefaultRegion, isNew, err := c.guessCloudInfo(sortedCloudNames, clouds, providerName, credName) 197 if err != nil { 198 return errors.Trace(err) 199 } 200 if defaultCloud == "" { 201 defaultCloud = defaultCloudNames[providerName] 202 } 203 credInfo.defaultCloudName = defaultCloud 204 if isNew { 205 credInfo.defaultCloudName = defaultCloudNames[providerName] 206 } 207 if (isNew || existingDefaultRegion == "") && detected.DefaultRegion != "" { 208 credInfo.region = detected.DefaultRegion 209 } 210 credInfo.isNew = isNew 211 discovered = append(discovered, credInfo) 212 } 213 } 214 } 215 if len(discovered) == 0 { 216 fmt.Fprintln(ctxt.Stderr, "No cloud credentials found.") 217 return nil 218 } 219 return c.interactiveCredentialsUpdate(ctxt, discovered) 220 } 221 222 // guessCloudInfo looks at all the compatible clouds for the provider name and 223 // looks to see whether the credential name exists already. 224 // The first match allows the default cloud and region to be set. The default 225 // cloud is used when prompting to save a credential. The sorted cloud names 226 // ensures that "aws" is preferred over "aws-china". 227 func (c *detectCredentialsCommand) guessCloudInfo( 228 sortedCloudNames []string, 229 clouds map[string]jujucloud.Cloud, 230 providerName, credName string, 231 ) (defaultCloud, defaultRegion string, isNew bool, _ error) { 232 isNew = true 233 for _, cloudName := range sortedCloudNames { 234 cloud := clouds[cloudName] 235 if cloud.Type != providerName { 236 continue 237 } 238 credentials, err := c.store.CredentialForCloud(cloudName) 239 if err != nil && !errors.IsNotFound(err) { 240 return "", "", false, errors.Trace(err) 241 } 242 if err != nil { 243 // None found. 244 continue 245 } 246 existingCredNames := set.NewStrings() 247 for name := range credentials.AuthCredentials { 248 existingCredNames.Add(name) 249 } 250 isNew = !existingCredNames.Contains(credName) 251 if defaultRegion == "" && credentials.DefaultRegion != "" { 252 defaultRegion = credentials.DefaultRegion 253 } 254 if defaultCloud == "" { 255 defaultCloud = cloudName 256 } 257 } 258 return defaultCloud, defaultRegion, isNew, nil 259 } 260 261 // interactiveCredentialsUpdate prints a list of the discovered credentials 262 // and prompts the user to update their local credentials. 263 func (c *detectCredentialsCommand) interactiveCredentialsUpdate(ctxt *cmd.Context, discovered []discoveredCredential) error { 264 for { 265 // Prompt for a credential to save. 266 c.printCredentialOptions(ctxt, discovered) 267 var input string 268 for { 269 var err error 270 input, err = c.promptCredentialNumber(ctxt.Stderr, ctxt.Stdin) 271 if err != nil { 272 return errors.Trace(err) 273 } 274 if strings.ToLower(input) == "q" { 275 return nil 276 } 277 if input != "" { 278 break 279 } 280 } 281 282 // Check the entered number. 283 num, err := strconv.Atoi(input) 284 if err != nil || num < 1 || num > len(discovered) { 285 fmt.Fprintf(ctxt.Stderr, "Invalid choice, enter a number between 1 and %v\n", len(discovered)) 286 continue 287 } 288 cred := discovered[num-1] 289 // Prompt for the cloud for which to save the credential. 290 cloudName, err := c.promptCloudName(ctxt.Stderr, ctxt.Stdin, cred.defaultCloudName, cred.cloudType) 291 if err != nil { 292 fmt.Fprintln(ctxt.Stderr, err.Error()) 293 continue 294 } 295 if cloudName == "" { 296 fmt.Fprintln(ctxt.Stderr, "No cloud name entered.") 297 continue 298 } 299 300 // Reading existing info so we can apply updated values. 301 existing, err := c.store.CredentialForCloud(cloudName) 302 if err != nil && !errors.IsNotFound(err) { 303 fmt.Fprintf(ctxt.Stderr, "error reading credential file: %v\n", err) 304 continue 305 } 306 if errors.IsNotFound(err) { 307 existing = &jujucloud.CloudCredential{ 308 AuthCredentials: make(map[string]jujucloud.Credential), 309 } 310 } 311 if cred.region != "" { 312 existing.DefaultRegion = cred.region 313 } 314 existing.AuthCredentials[cred.credentialName] = cred.credential 315 if err := c.store.UpdateCredential(cloudName, *existing); err != nil { 316 fmt.Fprintf(ctxt.Stderr, "error saving credential: %v\n", err) 317 } else { 318 // Update so we display correctly next time list is printed. 319 cred.isNew = false 320 discovered[num-1] = cred 321 fmt.Fprintf(ctxt.Stderr, "Saved %s to cloud %s\n", cred.credential.Label, cloudName) 322 } 323 } 324 } 325 326 func (c *detectCredentialsCommand) printCredentialOptions(ctxt *cmd.Context, discovered []discoveredCredential) { 327 fmt.Fprintln(ctxt.Stderr) 328 for i, cred := range discovered { 329 suffixText := " (existing, will overwrite)" 330 if cred.isNew { 331 suffixText = " (new)" 332 } 333 fmt.Fprintf(ctxt.Stderr, "%d. %s%s\n", i+1, cred.credential.Label, suffixText) 334 } 335 } 336 337 func (c *detectCredentialsCommand) promptCredentialNumber(out io.Writer, in io.Reader) (string, error) { 338 fmt.Fprint(out, "Select a credential to save by number, or type Q to quit: ") 339 defer out.Write([]byte{'\n'}) 340 input, err := readLine(in) 341 if err != nil { 342 return "", errors.Trace(err) 343 } 344 return strings.TrimSpace(input), nil 345 } 346 347 func (c *detectCredentialsCommand) promptCloudName(out io.Writer, in io.Reader, defaultCloudName, cloudType string) (string, error) { 348 text := fmt.Sprintf(`Select the cloud it belongs to, or type Q to quit [%s]: `, defaultCloudName) 349 fmt.Fprint(out, text) 350 defer out.Write([]byte{'\n'}) 351 input, err := readLine(in) 352 if err != nil { 353 return "", errors.Trace(err) 354 } 355 cloudName := strings.TrimSpace(input) 356 if strings.ToLower(cloudName) == "q" { 357 return "", nil 358 } 359 if cloudName == "" { 360 return defaultCloudName, nil 361 } 362 cloud, err := common.CloudOrProvider(cloudName, c.cloudByNameFunc) 363 if err != nil { 364 return "", err 365 } 366 if cloud.Type != cloudType { 367 return "", errors.Errorf("chosen credentials not compatible with a %s cloud", cloud.Type) 368 } 369 return cloudName, nil 370 } 371 372 func readLine(stdin io.Reader) (string, error) { 373 // Read one byte at a time to avoid reading beyond the delimiter. 374 line, err := bufio.NewReader(byteAtATimeReader{stdin}).ReadString('\n') 375 if err != nil { 376 return "", errors.Trace(err) 377 } 378 return line[:len(line)-1], nil 379 } 380 381 type byteAtATimeReader struct { 382 io.Reader 383 } 384 385 func (r byteAtATimeReader) Read(out []byte) (int, error) { 386 return r.Reader.Read(out[:1]) 387 }