Skip to content

Commit 9d470ef

Browse files
committed
fix(huawei): fix CTS endpoint and region handling
1 parent b9494c7 commit 9d470ef

16 files changed

Lines changed: 410 additions & 38 deletions

File tree

pkg/providers/huawei/api/projects.go

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,97 @@ type ProjectNotFoundError struct {
1414
}
1515

1616
func (e *ProjectNotFoundError) Error() string {
17-
if e == nil {
18-
return "project not found"
19-
}
20-
return fmt.Sprintf("project not found for region %s", strings.TrimSpace(e.Region))
17+
return "project not found"
2118
}
2219

2320
func IsProjectNotFound(err error) bool {
2421
var target *ProjectNotFoundError
2522
return errors.As(err, &target)
2623
}
2724

25+
type ProjectCatalog struct {
26+
byRegion map[string]string
27+
}
28+
29+
// NewProjectCatalog indexes IAM projects by region name, preferring projects
30+
// in domainID when duplicate names are present.
31+
func NewProjectCatalog(projects []IAMProject, domainID string) *ProjectCatalog {
32+
domainID = strings.TrimSpace(domainID)
33+
matched := make(map[string]string, len(projects))
34+
fallback := make(map[string]string)
35+
for _, project := range projects {
36+
region := strings.TrimSpace(project.Name)
37+
projectID := strings.TrimSpace(project.ID)
38+
if region == "" || projectID == "" {
39+
continue
40+
}
41+
if domainID == "" || strings.TrimSpace(project.DomainID) == domainID {
42+
if matched[region] == "" {
43+
matched[region] = projectID
44+
}
45+
continue
46+
}
47+
if fallback[region] == "" {
48+
fallback[region] = projectID
49+
}
50+
}
51+
for region, projectID := range fallback {
52+
if matched[region] == "" {
53+
matched[region] = projectID
54+
}
55+
}
56+
return &ProjectCatalog{byRegion: matched}
57+
}
58+
59+
// ProjectID returns the project ID for region when the catalog contains it.
60+
func (c *ProjectCatalog) ProjectID(region string) (string, bool) {
61+
if c == nil {
62+
return "", false
63+
}
64+
projectID := strings.TrimSpace(c.byRegion[strings.TrimSpace(region)])
65+
return projectID, projectID != ""
66+
}
67+
68+
// FilterRegions keeps only regions that exist in the catalog.
69+
func (c *ProjectCatalog) FilterRegions(regions []string) []string {
70+
if c == nil {
71+
return append([]string(nil), regions...)
72+
}
73+
filtered := make([]string, 0, len(regions))
74+
for _, region := range regions {
75+
if _, ok := c.ProjectID(region); ok {
76+
filtered = append(filtered, region)
77+
}
78+
}
79+
return filtered
80+
}
81+
82+
// ListAccessibleProjectCatalog fetches the project list visible to the current
83+
// credential and indexes it by region name.
84+
func ListAccessibleProjectCatalog(ctx context.Context, client *Client, domainID string) (*ProjectCatalog, error) {
85+
if client == nil {
86+
return nil, fmt.Errorf("huawei project catalog: nil client")
87+
}
88+
89+
controlPlaneRegion := strings.TrimSpace(client.credential.Region)
90+
if controlPlaneRegion == "" || controlPlaneRegion == "all" {
91+
return nil, fmt.Errorf("huawei project catalog: unresolved control plane region %q", controlPlaneRegion)
92+
}
93+
94+
var resp ListProjectsResponse
95+
if err := client.DoJSON(ctx, Request{
96+
Service: "iam",
97+
Region: controlPlaneRegion,
98+
Intl: client.credential.Intl,
99+
Method: http.MethodGet,
100+
Path: "/v3/auth/projects",
101+
Idempotent: true,
102+
}, &resp); err != nil {
103+
return nil, err
104+
}
105+
return NewProjectCatalog(resp.Projects, domainID), nil
106+
}
107+
28108
// ResolveProjectID maps a region name to its project ID via the IAM control
29109
// plane. The client must be configured with a concrete control-plane region.
30110
func ResolveProjectID(ctx context.Context, client *Client, domainID, targetRegion string) (string, error) {

pkg/providers/huawei/cts/cts.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ type Driver struct {
3030
DomainID string
3131
Client *api.Client
3232
projectID map[string]string
33+
34+
ProjectCatalog *api.ProjectCatalog
3335
}
3436

3537
func (d *Driver) client() *api.Client {
@@ -127,6 +129,12 @@ func (d *Driver) listRegionEvents(ctx context.Context, region, sourceFilter stri
127129
}
128130

129131
func (d *Driver) resolveProjectID(ctx context.Context, region string) (string, error) {
132+
if projectID, ok := d.ProjectCatalog.ProjectID(region); ok {
133+
return projectID, nil
134+
}
135+
if d.ProjectCatalog != nil {
136+
return "", &api.ProjectNotFoundError{Region: region}
137+
}
130138
if d.projectID == nil {
131139
d.projectID = make(map[string]string)
132140
}
@@ -162,6 +170,9 @@ func (d *Driver) resolveRegions() []string {
162170
}
163171

164172
region := strings.TrimSpace(d.Cred.Region)
173+
if d.ProjectCatalog != nil {
174+
return nil
175+
}
165176
if region == "" || region == "all" {
166177
region = defaultRegion
167178
}

pkg/providers/huawei/ecs/instances.go

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ type Driver struct {
2323
Regions []string
2424
DomainID string
2525
Client *api.Client
26+
27+
ProjectCatalog *api.ProjectCatalog
2628
}
2729

2830
func (d *Driver) client() *api.Client {
@@ -50,12 +52,17 @@ func (d *Driver) GetResource(ctx context.Context) ([]schema.Host, error) {
5052
probeRegion := regions[0]
5153
probeItems, probeErr := d.listRegion(ctx, probeRegion)
5254
if probeErr != nil {
53-
if api.IsAccessDenied(probeErr) {
55+
switch {
56+
case api.IsProjectNotFound(probeErr):
57+
// Some public service regions may not have an account project.
58+
// Treat them as not applicable instead of polluting output.
59+
case api.IsAccessDenied(probeErr):
5460
return list, probeErr
61+
default:
62+
seedErrs[probeRegion] = probeErr
63+
tracker.Update(probeRegion, 0)
64+
trackerUsed = true
5565
}
56-
seedErrs[probeRegion] = probeErr
57-
tracker.Update(probeRegion, 0)
58-
trackerUsed = true
5966
} else {
6067
list = append(list, probeItems...)
6168
tracker.Update(probeRegion, len(probeItems))
@@ -69,14 +76,21 @@ func (d *Driver) GetResource(ctx context.Context) ([]schema.Host, error) {
6976

7077
trackerUsed = true
7178
got, regionErrs := regionrun.ForEach(ctx, regions, 0, tracker, func(ctx context.Context, r string) ([]schema.Host, error) {
72-
return d.listRegion(ctx, r)
79+
items, err := d.listRegion(ctx, r)
80+
if err != nil {
81+
if api.IsProjectNotFound(err) {
82+
return nil, regionrun.SkipRegion()
83+
}
84+
return nil, err
85+
}
86+
return items, nil
7387
})
7488
list = append(list, got...)
7589
return list, regionrun.Wrap(mergeRegionErrors(seedErrs, regionErrs))
7690
}
7791

7892
func (d *Driver) listRegion(ctx context.Context, region string) ([]schema.Host, error) {
79-
projectID, err := api.ResolveProjectID(ctx, d.client(), d.DomainID, region)
93+
projectID, err := d.resolveProjectID(ctx, region)
8094
if err != nil {
8195
return nil, err
8296
}
@@ -127,6 +141,16 @@ func (d *Driver) listRegion(ctx context.Context, region string) ([]schema.Host,
127141
return items, err
128142
}
129143

144+
func (d *Driver) resolveProjectID(ctx context.Context, region string) (string, error) {
145+
if projectID, ok := d.ProjectCatalog.ProjectID(region); ok {
146+
return projectID, nil
147+
}
148+
if d.ProjectCatalog != nil {
149+
return "", &api.ProjectNotFoundError{Region: region}
150+
}
151+
return api.ResolveProjectID(ctx, d.client(), d.DomainID, region)
152+
}
153+
130154
func mergeRegionErrors(base, extra map[string]error) map[string]error {
131155
if len(base) == 0 && len(extra) == 0 {
132156
return nil

pkg/providers/huawei/ecs/instances_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,44 @@ func TestDriverGetResourceAggregatesRegionErrorsAndContinues(t *testing.T) {
9191
}
9292
}
9393

94+
func TestDriverGetResourceSkipsProjectNotFoundRegions(t *testing.T) {
95+
transport := &routingTransport{
96+
routes: map[string]routeResponse{
97+
"GET iam.cn-north-4.myhuaweicloud.com /v3/projects?name=cn-east-201": {
98+
body: `{"projects":[]}`,
99+
},
100+
"GET iam.cn-north-4.myhuaweicloud.com /v3/projects?name=cn-north-4": {
101+
body: `{"projects":[{"id":"project-n4","name":"cn-north-4","domain_id":"d-1","enabled":true}]}`,
102+
},
103+
"GET ecs.cn-north-4.myhuaweicloud.com /v1/project-n4/cloudservers/detail?limit=100&offset=1": {
104+
body: `{"count":1,"servers":[{"id":"i-uuid-ok","status":"ACTIVE","name":"ecs-ok","addresses":{"net-a":[{"addr":"10.0.0.3","OS-EXT-IPS:type":"fixed"}]}}]}`,
105+
},
106+
"GET iam.cn-north-4.myhuaweicloud.com /v3/projects?name=cn-south-201": {
107+
body: `{"projects":[]}`,
108+
},
109+
},
110+
}
111+
112+
driver := newTestDriver([]string{"cn-east-201", "cn-north-4", "cn-south-201"}, "d-1", transport)
113+
var (
114+
got []schema.Host
115+
err error
116+
output string
117+
)
118+
output = captureStdout(t, func() {
119+
got, err = driver.GetResource(context.Background())
120+
})
121+
if err != nil {
122+
t.Fatalf("GetResource() error = %v", err)
123+
}
124+
if len(got) != 1 || got[0].HostName != "ecs-ok" || got[0].Region != "cn-north-4" {
125+
t.Fatalf("unexpected hosts: %+v", got)
126+
}
127+
if strings.Contains(output, "cn-east-201") || strings.Contains(output, "cn-south-201") {
128+
t.Fatalf("skipped regions should not be printed: %q", output)
129+
}
130+
}
131+
94132
func newTestDriver(regions []string, domainID string, transport http.RoundTripper) *Driver {
95133
cred := auth.New("AKID", "SECRET", "cn-north-4", false)
96134
return &Driver{

pkg/providers/huawei/endpoint/endpoint.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,26 @@
1010
// Huawei Cloud endpoints follow a uniform pattern for service+region
1111
// combinations:
1212
//
13-
// https://{service}.{region}.myhuaweicloud.com
13+
// https://{service}.{region}.myhuaweicloud.com
1414
//
15-
// with a small number of services (bss, iam) historically served from global
16-
// or partition-specific hosts. Those exceptions are encoded here.
15+
// with a small number of global, partition-specific, or regional endpoint
16+
// exceptions. Those exceptions are encoded here.
1717
package endpoint
1818

1919
import "fmt"
2020

21+
type endpointKey struct {
22+
service string
23+
region string
24+
}
25+
26+
var regionalEndpointOverrides = map[endpointKey]string{
27+
{service: "cts", region: "eu-west-101"}: "https://cts.eu-west-101.myhuaweicloud.eu",
28+
{service: "iam", region: "eu-west-101"}: "https://iam.eu-west-101.myhuaweicloud.eu",
29+
{service: "lts", region: "eu-west-101"}: "https://lts.eu-west-101.myhuaweicloud.eu",
30+
{service: "rds", region: "eu-west-101"}: "https://rds.eu-west-101.myhuaweicloud.eu",
31+
}
32+
2133
// For returns the endpoint URL to pass to WithEndpoint() for the given
2234
// (service, region) pair. intl toggles between mainland China and
2335
// international billing partitions for the handful of services where they
@@ -32,5 +44,8 @@ func For(service, region string, intl bool) string {
3244
case "obs":
3345
return fmt.Sprintf("https://obs.%s.myhuaweicloud.com", region)
3446
}
47+
if endpoint, ok := regionalEndpointOverrides[endpointKey{service: service, region: region}]; ok {
48+
return endpoint
49+
}
3550
return fmt.Sprintf("https://%s.%s.myhuaweicloud.com", service, region)
3651
}

pkg/providers/huawei/endpoint/endpoint_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ func TestFor(t *testing.T) {
1414
{"ecs unknown region (no panic)", "ecs", "cn-zzz-99", false, "https://ecs.cn-zzz-99.myhuaweicloud.com"},
1515
{"rds ap", "rds", "ap-southeast-3", false, "https://rds.ap-southeast-3.myhuaweicloud.com"},
1616
{"iam default", "iam", "cn-north-1", false, "https://iam.cn-north-1.myhuaweicloud.com"},
17+
{"iam dublin", "iam", "eu-west-101", false, "https://iam.eu-west-101.myhuaweicloud.eu"},
18+
{"cts dublin", "cts", "eu-west-101", false, "https://cts.eu-west-101.myhuaweicloud.eu"},
19+
{"lts dublin", "lts", "eu-west-101", false, "https://lts.eu-west-101.myhuaweicloud.eu"},
20+
{"rds dublin", "rds", "eu-west-101", false, "https://rds.eu-west-101.myhuaweicloud.eu"},
1721
{"bss domestic", "bss", "any", false, "https://bss.myhuaweicloud.com"},
1822
{"bss intl", "bss", "any", true, "https://bss-intl.myhuaweicloud.com"},
1923
{"obs explicit", "obs", "cn-north-4", false, "https://obs.cn-north-4.myhuaweicloud.com"},

0 commit comments

Comments
 (0)