Skip to content

Commit f3690c0

Browse files
author
Joshua Reed
committed
Init.
1 parent 3247e55 commit f3690c0

File tree

3 files changed

+298
-0
lines changed

3 files changed

+298
-0
lines changed

pkg/cloud/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Client interface {
3434
TagIface
3535
ZoneIFace
3636
IsoNetworkIface
37+
UserCredIFace
3738
}
3839

3940
type client struct {

pkg/cloud/user_credentials.go

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cloud
18+
19+
import (
20+
"strings"
21+
22+
"github.com/pkg/errors"
23+
)
24+
25+
type UserCredIFace interface {
26+
ResolveDomain(*Domain) error
27+
ResolveAccount(*Account) error
28+
ResolveUser(*User) error
29+
ResolveUserKeys(*User) error
30+
}
31+
32+
// Domain contains specifications that identify a domain.
33+
type Domain struct {
34+
Name string
35+
Path string
36+
ID string
37+
}
38+
39+
// Account contains specifications that identify an account.
40+
type Account struct {
41+
Name string
42+
Domain Domain
43+
ID string
44+
}
45+
46+
// User contains information uniquely identifying and scoping a user.
47+
type User struct {
48+
ID string
49+
Name string
50+
APIKey string
51+
SecretKey string
52+
Account
53+
}
54+
55+
// ResolveDomain resolves a domain's information.
56+
func (c *client) ResolveDomain(domain *Domain) error {
57+
// A domain can be specified by Id, Name, and or Path.
58+
// Parse path and use it to set name if not present.
59+
tokens := []string{}
60+
if domain.Path != "" {
61+
// Split path and get name.
62+
tokens = strings.Split(domain.Path, domainDelimiter)
63+
if domain.Name == "" {
64+
domain.Name = tokens[len(tokens)-1]
65+
}
66+
// Ensure the path begins with ROOT.
67+
if !strings.EqualFold(tokens[0], "root") {
68+
tokens = append([]string{"ROOT"}, tokens...)
69+
} else {
70+
tokens[0] = "ROOT"
71+
domain.Path = strings.Join(tokens, domainDelimiter)
72+
}
73+
}
74+
75+
// Set present search/list parameters.
76+
p := c.cs.Domain.NewListDomainsParams()
77+
p.SetListall(true)
78+
setIfNotEmpty(domain.Name, p.SetName)
79+
setIfNotEmpty(domain.ID, p.SetId)
80+
81+
// If path was provided also use level to search for domain.
82+
if level := len(tokens) - 1; level >= 0 {
83+
p.SetLevel(level)
84+
}
85+
86+
resp, retErr := c.cs.Domain.ListDomains(p)
87+
if retErr != nil {
88+
return retErr
89+
}
90+
91+
// If the Id was provided.
92+
if domain.ID != "" {
93+
if resp.Count != 1 {
94+
return errors.Errorf("domain ID %s provided, expected exactly one domain, got %d", domain.ID, resp.Count)
95+
}
96+
if domain.Path != "" && !strings.EqualFold(resp.Domains[0].Path, domain.Path) {
97+
return errors.Errorf("domain Path %s did not match domain ID %s", domain.Path, domain.ID)
98+
}
99+
domain.Path = resp.Domains[0].Path
100+
domain.Name = resp.Domains[0].Name
101+
return nil
102+
}
103+
104+
// Consider the case where only the domain name is provided.
105+
if domain.Path == "" && domain.Name != "" {
106+
if resp.Count != 1 {
107+
return errors.Errorf(
108+
"only domain name: %s provided, expected exactly one domain, got %d", domain.Name, resp.Count)
109+
}
110+
}
111+
112+
// Finally, search for the domain by Path.
113+
for _, possibleDomain := range resp.Domains {
114+
if possibleDomain.Path == domain.Path {
115+
domain.ID = possibleDomain.Id
116+
return nil
117+
}
118+
}
119+
120+
return errors.Errorf("domain not found for domain path %s", domain.Path)
121+
}
122+
123+
// ResolveAccount resolves an account's information.
124+
func (c *client) ResolveAccount(account *Account) error {
125+
// Resolve domain prior to any account resolution activity.
126+
if err := c.ResolveDomain(&account.Domain); err != nil {
127+
return errors.Wrap(err, "error encountered when resolving domain details")
128+
}
129+
130+
p := c.cs.Account.NewListAccountsParams()
131+
p.SetDomainid(account.Domain.ID)
132+
setIfNotEmpty(account.ID, p.SetId)
133+
setIfNotEmpty(account.Name, p.SetName)
134+
resp, retErr := c.cs.Account.ListAccounts(p)
135+
if retErr != nil {
136+
return retErr
137+
} else if resp.Count != 1 {
138+
return errors.Errorf("expected 1 Account with account name %s in domain ID %s, but got %d",
139+
account.Name, account.Domain.ID, resp.Count)
140+
}
141+
account.ID = resp.Accounts[0].Id
142+
account.Name = resp.Accounts[0].Name
143+
return nil
144+
}
145+
146+
// ResolveUser resolves a user's information.
147+
func (c *client) ResolveUser(user *User) error {
148+
// Resolve account prior to any user resolution activity.
149+
if err := c.ResolveAccount(&user.Account); err != nil {
150+
return errors.Wrap(err, "error encountered when resolving account details")
151+
}
152+
153+
p := c.cs.User.NewListUsersParams()
154+
p.SetAccount(user.Account.Name)
155+
p.SetDomainid(user.Domain.ID)
156+
p.SetListall(true)
157+
resp, err := c.cs.User.ListUsers(p)
158+
if err != nil {
159+
return err
160+
} else if resp.Count != 1 {
161+
return errors.Errorf("expected 1 User with username %s but got %d", user.Name, resp.Count)
162+
}
163+
164+
user.ID = resp.Users[0].Id
165+
user.Name = resp.Users[0].Username
166+
167+
return nil
168+
}
169+
170+
// ResolveUserKeys resolves a user's api keys.
171+
func (c *client) ResolveUserKeys(user *User) error {
172+
// Resolve user prior to any api key resolution activity.
173+
if err := c.ResolveUser(user); err != nil {
174+
return errors.Wrap(err, "error encountered when resolving user details")
175+
}
176+
177+
p := c.cs.User.NewGetUserKeysParams(user.ID)
178+
resp, err := c.cs.User.GetUserKeys(p)
179+
if err != nil {
180+
return errors.Errorf("error encountered when resolving user api keys for user %s", user.Name)
181+
}
182+
user.APIKey = resp.Apikey
183+
user.SecretKey = resp.Secretkey
184+
return nil
185+
}
186+
187+
// GetUserWithKeys will search a domain and account for the first user that has api keys.
188+
// Returns true if a user is found and false otherwise.
189+
func (c *client) GetUserWithKeys(user *User) (error, bool) {
190+
// Resolve account prior to any user resolution activity.
191+
if err := c.ResolveAccount(&user.Account); err != nil {
192+
return errors.Wrap(err, "error encountered when resolving account details"), false
193+
}
194+
195+
// List users and take first user that has already has api keys.
196+
p := c.cs.User.NewListUsersParams()
197+
p.SetAccount(user.Account.Name)
198+
p.SetDomainid(user.Domain.ID)
199+
p.SetListall(true)
200+
resp, err := c.cs.User.ListUsers(p)
201+
if err != nil {
202+
return err, false
203+
}
204+
205+
// Return first user with keys.
206+
for _, possibleUser := range resp.Users {
207+
user.ID = possibleUser.Id
208+
if err := c.ResolveUserKeys(user); err == nil {
209+
return nil, true
210+
}
211+
}
212+
user.ID = ""
213+
return nil, false
214+
}

pkg/cloud/user_credentials_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package cloud_test
18+
19+
import (
20+
"github.com/aws/cluster-api-provider-cloudstack/pkg/cloud"
21+
"github.com/aws/cluster-api-provider-cloudstack/test/dummies"
22+
. "github.com/onsi/ginkgo"
23+
"github.com/pkg/errors"
24+
25+
. "github.com/onsi/gomega"
26+
)
27+
28+
var _ = Describe("User Credentials", func() {
29+
30+
BeforeEach(func() {
31+
dummies.SetDummyVars()
32+
dummies.SetDummyClusterStatus()
33+
dummies.SetDummyCSMachineStatuses()
34+
})
35+
36+
AfterEach(func() {
37+
})
38+
39+
Context("UserCred Semi-Integ Tests", func() {
40+
client, connectionErr := cloud.NewClient("../../cloud-config")
41+
var domain cloud.Domain
42+
var account cloud.Account
43+
var user cloud.User
44+
45+
BeforeEach(func() {
46+
if connectionErr != nil { // Only do these tests if an actual ACS instance is available via cloud-config.
47+
Skip(errors.Wrapf(connectionErr, "Could not connect to ACS instance.").Error())
48+
}
49+
50+
// Settup dummies.
51+
// TODO: move these to the test dummies package.
52+
domain = cloud.Domain{Path: "ROOT/blah/blah/subsub"}
53+
account = cloud.Account{Name: "SuperNested", Domain: domain}
54+
user = cloud.User{Name: "SubSub", Account: account}
55+
})
56+
57+
It("can resolve a domain from the path", func() {
58+
Ω(client.ResolveDomain(&domain)).Should(Succeed())
59+
Ω(domain.ID).ShouldNot(BeEmpty())
60+
})
61+
62+
It("can resolve an account from the domain path and account name", func() {
63+
Ω(client.ResolveAccount(&account)).Should(Succeed())
64+
Ω(account.ID).ShouldNot(BeEmpty())
65+
})
66+
67+
It("can resolve a user from the domain path, account name, and user name", func() {
68+
Ω(client.ResolveUser(&user)).Should(Succeed())
69+
Ω(user.ID).ShouldNot(BeEmpty())
70+
})
71+
72+
It("can get sub-domain user's credentials", func() {
73+
Ω(client.ResolveUserKeys(&user)).Should(Succeed())
74+
75+
Ω(user.APIKey).ShouldNot(BeEmpty())
76+
Ω(user.SecretKey).ShouldNot(BeEmpty())
77+
})
78+
79+
It("can get an arbitrary user with keys from domain and account specifications alone", func() {
80+
Ω(client.ResolveUserKeys(&user)).Should(Succeed())
81+
})
82+
})
83+
})

0 commit comments

Comments
 (0)