Skip to content

Commit 5a3cd3a

Browse files
committed
Update FinalizeCredential, use in add-credential
This diff updates the FinalizeCredential provider method to take cloud endpoints, so that providers can communicate with the cloud in order to finalize the credential. Specifically, Azure needs to connect to Active Directory to perform interactive authentication, and then configure the service principal.
1 parent c025807 commit 5a3cd3a

23 files changed

+286
-75
lines changed

cmd/juju/cloud/addcredential.go

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,39 @@ func (c *addCredentialCommand) interactiveAddCredential(ctxt *cmd.Context, schem
215215
if err != nil {
216216
return errors.Trace(err)
217217
}
218-
newCredential := jujucloud.NewCredential(authType, attrs)
219-
existingCredentials.AuthCredentials[credentialName] = newCredential
218+
219+
cloudEndpoint := c.cloud.Endpoint
220+
cloudIdentityEndpoint := c.cloud.IdentityEndpoint
221+
if len(c.cloud.Regions) > 0 {
222+
// NOTE(axw) we use the first region in the cloud,
223+
// because this is all we need for Azure right now.
224+
// Each region has the same endpoints, so it does
225+
// not matter which one we use. If we expand
226+
// credential generation to other providers, and
227+
// they do have region-specific endpoints, then we
228+
// should prompt the user for the region to use.
229+
// That would be better left to the provider, though.
230+
region := c.cloud.Regions[0]
231+
cloudEndpoint = region.Endpoint
232+
cloudIdentityEndpoint = region.IdentityEndpoint
233+
}
234+
235+
credentialsProvider, err := environs.Provider(c.cloud.Type)
236+
if err != nil {
237+
return errors.Trace(err)
238+
}
239+
newCredential, err := credentialsProvider.FinalizeCredential(
240+
ctxt, environs.FinalizeCredentialParams{
241+
Credential: jujucloud.NewCredential(authType, attrs),
242+
CloudEndpoint: cloudEndpoint,
243+
CloudIdentityEndpoint: cloudIdentityEndpoint,
244+
},
245+
)
246+
if err != nil {
247+
return errors.Annotate(err, "finalizing credential")
248+
}
249+
250+
existingCredentials.AuthCredentials[credentialName] = *newCredential
220251
err = c.store.UpdateCredential(c.CloudName, *existingCredentials)
221252
if err != nil {
222253
return errors.Trace(err)

cmd/juju/cloud/addcredential_test.go

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,10 @@ func (s *addCredentialSuite) SetUpSuite(c *gc.C) {
4444
return nil, errors.NotFoundf("cloud %v", cloud)
4545
}
4646
return &jujucloud.Cloud{
47-
Type: "mock-addcredential-provider",
48-
AuthTypes: s.authTypes,
47+
Type: "mock-addcredential-provider",
48+
AuthTypes: s.authTypes,
49+
Endpoint: "cloud-endpoint",
50+
IdentityEndpoint: "cloud-identity-endpoint",
4951
}, nil
5052
}
5153
}
@@ -207,6 +209,36 @@ func (s *addCredentialSuite) TestAddCredentialMultipleAuthType(c *gc.C) {
207209
s.assertAddUserpassCredential(c, "fred\nuserpass\nuser\npassword\n", nil)
208210
}
209211

212+
func (s *addCredentialSuite) TestAddCredentialInteractive(c *gc.C) {
213+
s.authTypes = []jujucloud.AuthType{"interactive"}
214+
s.schema = map[jujucloud.AuthType]jujucloud.CredentialSchema{
215+
"interactive": {{"username", jujucloud.CredentialAttr{}}},
216+
}
217+
218+
stdin := strings.NewReader("bobscreds\nbob\n")
219+
ctx, err := s.run(c, stdin, "somecloud")
220+
c.Assert(err, jc.ErrorIsNil)
221+
222+
c.Assert(testing.Stderr(ctx), gc.Equals, `
223+
Enter credential name: Using auth-type "interactive".
224+
Enter username: generating userpass credential
225+
`[1:])
226+
227+
// FinalizeCredential should have generated a userpass credential
228+
// based on the input from the interactive credential.
229+
c.Assert(s.store.Credentials, jc.DeepEquals, map[string]jujucloud.CloudCredential{
230+
"somecloud": {
231+
AuthCredentials: map[string]jujucloud.Credential{
232+
"bobscreds": jujucloud.NewCredential(jujucloud.UserPassAuthType, map[string]string{
233+
"username": "bob",
234+
"password": "cloud-endpoint",
235+
"application-password": "cloud-identity-endpoint",
236+
}),
237+
},
238+
},
239+
})
240+
}
241+
210242
func (s *addCredentialSuite) TestAddCredentialReplace(c *gc.C) {
211243
s.store.Credentials = map[string]jujucloud.CloudCredential{
212244
"somecloud": {

cmd/juju/cloud/detectcredentials_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,22 @@ func (p *mockProvider) CredentialSchemas() map[jujucloud.AuthType]jujucloud.Cred
7676
return *p.credSchemas
7777
}
7878

79+
func (p *mockProvider) FinalizeCredential(
80+
ctx environs.FinalizeCredentialContext,
81+
args environs.FinalizeCredentialParams,
82+
) (*jujucloud.Credential, error) {
83+
if args.Credential.AuthType() == "interactive" {
84+
fmt.Fprintln(ctx.GetStderr(), "generating userpass credential")
85+
out := jujucloud.NewCredential(jujucloud.UserPassAuthType, map[string]string{
86+
"username": args.Credential.Attributes()["username"],
87+
"password": args.CloudEndpoint,
88+
"application-password": args.CloudIdentityEndpoint,
89+
})
90+
return &out, nil
91+
}
92+
return &args.Credential, nil
93+
}
94+
7995
func (s *detectCredentialsSuite) SetUpSuite(c *gc.C) {
8096
environs.RegisterProvider("mock-provider", &mockProvider{detectedCreds: &s.aCredential})
8197
}

cmd/juju/commands/bootstrap.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -421,7 +421,12 @@ func (c *bootstrapCommand) Run(ctx *cmd.Context) (resultErr error) {
421421
store := c.ClientStore()
422422
var detectedCredentialName string
423423
credential, credentialName, regionName, err := modelcmd.GetCredentials(
424-
ctx, store, c.Region, c.CredentialName, c.Cloud, cloud.Type,
424+
ctx, store, modelcmd.GetCredentialsParams{
425+
Cloud: *cloud,
426+
CloudName: c.Cloud,
427+
CloudRegion: c.Region,
428+
CredentialName: c.CredentialName,
429+
},
425430
)
426431
if errors.Cause(err) == modelcmd.ErrMultipleCredentials {
427432
return ambiguousCredentialError

cmd/juju/controller/addmodel.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,8 +426,12 @@ func (c *addModelCommand) maybeUploadCredential(
426426

427427
// Upload the credential from the client, if it exists locally.
428428
credential, _, _, err := modelcmd.GetCredentials(
429-
ctx, c.ClientStore(), c.CloudRegion, credentialTag.Name(),
430-
cloudTag.Id(), cloud.Type,
429+
ctx, c.ClientStore(), modelcmd.GetCredentialsParams{
430+
Cloud: cloud,
431+
CloudName: cloudTag.Id(),
432+
CloudRegion: c.CloudRegion,
433+
CredentialName: credentialTag.Name(),
434+
},
431435
)
432436
if err != nil {
433437
return names.CloudCredentialTag{}, errors.Trace(err)

cmd/juju/controller/listcontrollersformatters.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ func formatControllersTabular(writer io.Writer, set ControllerSet, promptRefresh
3535
w := output.Wrapper{tw}
3636

3737
if promptRefresh && len(set.Controllers) > 0 {
38-
fmt.Fprintln(writer, "Use --refresh to see the latest information.\n")
38+
fmt.Fprintln(writer, "Use --refresh to see the latest information.")
39+
fmt.Fprintln(writer)
3940
}
4041
w.Println("CONTROLLER", "MODEL", "USER", "ACCESS", "CLOUD/REGION", "MODELS", "MACHINES", "HA", "VERSION")
4142
tw.SetColumnAlignRight(5)

cmd/modelcmd/base.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -392,13 +392,26 @@ func (g bootstrapConfigGetter) getBootstrapConfigParams(controllerName string) (
392392

393393
var credential *cloud.Credential
394394
if bootstrapConfig.Credential != "" {
395+
bootstrapCloud := cloud.Cloud{
396+
Type: bootstrapConfig.CloudType,
397+
Endpoint: bootstrapConfig.CloudEndpoint,
398+
IdentityEndpoint: bootstrapConfig.CloudIdentityEndpoint,
399+
}
400+
if bootstrapConfig.CloudRegion != "" {
401+
bootstrapCloud.Regions = []cloud.Region{{
402+
Name: bootstrapConfig.CloudRegion,
403+
Endpoint: bootstrapConfig.CloudEndpoint,
404+
IdentityEndpoint: bootstrapConfig.CloudIdentityEndpoint,
405+
}}
406+
}
395407
credential, _, _, err = GetCredentials(
396-
g.ctx,
397-
g.store,
398-
bootstrapConfig.CloudRegion,
399-
bootstrapConfig.Credential,
400-
bootstrapConfig.Cloud,
401-
bootstrapConfig.CloudType,
408+
g.ctx, g.store,
409+
GetCredentialsParams{
410+
Cloud: bootstrapCloud,
411+
CloudName: bootstrapConfig.Cloud,
412+
CloudRegion: bootstrapConfig.CloudRegion,
413+
CredentialName: bootstrapConfig.Credential,
414+
},
402415
)
403416
if err != nil {
404417
return nil, nil, errors.Trace(err)

cmd/modelcmd/credentials.go

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,26 +21,62 @@ var (
2121
ErrMultipleCredentials = errors.New("more than one credential detected")
2222
)
2323

24+
// GetCredentialsParams contains parameters for the GetCredentials function.
25+
type GetCredentialsParams struct {
26+
// Cloud is the cloud definition.
27+
Cloud cloud.Cloud
28+
29+
// CloudName is the name of the cloud for which credentials are being
30+
// obtained.
31+
CloudName string
32+
33+
// CloudRegion is the name of the region that the user has specified.
34+
// If this is empty, then GetCredentials will determine the default
35+
// region, and return that. The default region is the one set by the
36+
// user in credentials.yaml, or if there is none set, the first region
37+
// in the cloud's list.
38+
CloudRegion string
39+
40+
// CredentialName is the name of the credential to get.
41+
CredentialName string
42+
}
43+
2444
// GetCredentials returns a curated set of credential values for a given cloud.
2545
// The credential key values are read from the credentials store and the provider
2646
// finalises the values to resolve things like json files.
2747
// If region is not specified, the default credential region is used.
2848
func GetCredentials(
2949
ctx *cmd.Context,
3050
store jujuclient.CredentialGetter,
31-
region, credentialName, cloudName, cloudType string,
51+
args GetCredentialsParams,
3252
) (_ *cloud.Credential, chosenCredentialName, regionName string, _ error) {
3353

3454
credential, credentialName, defaultRegion, err := credentialByName(
35-
store, cloudName, credentialName,
55+
store, args.CloudName, args.CredentialName,
3656
)
3757
if err != nil {
3858
return nil, "", "", errors.Trace(err)
3959
}
4060

41-
regionName = region
61+
regionName = args.CloudRegion
4262
if regionName == "" {
4363
regionName = defaultRegion
64+
if regionName == "" && len(args.Cloud.Regions) > 0 {
65+
// No region was specified, use the first region
66+
// in the list.
67+
regionName = args.Cloud.Regions[0].Name
68+
}
69+
}
70+
71+
cloudEndpoint := args.Cloud.Endpoint
72+
cloudIdentityEndpoint := args.Cloud.IdentityEndpoint
73+
if regionName != "" {
74+
region, err := cloud.RegionByName(args.Cloud.Regions, regionName)
75+
if err != nil {
76+
return nil, "", "", errors.Trace(err)
77+
}
78+
cloudEndpoint = region.Endpoint
79+
cloudIdentityEndpoint = region.IdentityEndpoint
4480
}
4581

4682
readFile := func(f string) ([]byte, error) {
@@ -52,7 +88,7 @@ func GetCredentials(
5288
}
5389

5490
// Finalize credential against schemas supported by the provider.
55-
provider, err := environs.Provider(cloudType)
91+
provider, err := environs.Provider(args.Cloud.Type)
5692
if err != nil {
5793
return nil, "", "", errors.Trace(err)
5894
}
@@ -63,17 +99,25 @@ func GetCredentials(
6399
if err != nil {
64100
return nil, "", "", errors.Annotatef(
65101
err, "finalizing %q credential for cloud %q",
66-
credentialName, cloudName,
102+
credentialName, args.CloudName,
67103
)
68104
}
69-
finalizedCredential, err := provider.FinalizeCredential(ctx, *credential)
105+
106+
credential, err = provider.FinalizeCredential(
107+
ctx, environs.FinalizeCredentialParams{
108+
Credential: *credential,
109+
CloudEndpoint: cloudEndpoint,
110+
CloudIdentityEndpoint: cloudIdentityEndpoint,
111+
},
112+
)
70113
if err != nil {
71114
return nil, "", "", errors.Annotatef(
72115
err, "finalizing %q credential for cloud %q",
73-
credentialName, cloudName,
116+
credentialName, args.CloudName,
74117
)
75118
}
76-
return &finalizedCredential, credentialName, regionName, nil
119+
120+
return credential, credentialName, regionName, nil
77121
}
78122

79123
// credentialByName returns the credential and default region to use for the

0 commit comments

Comments
 (0)