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