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