Skip to content

Commit 9b12642

Browse files
committed
fix: Use the right kind of public cloud loadbalancer
Signed-off-by: Arthur Amstutz <arthur.amstutz@corp.ovh.com>
1 parent c94bf81 commit 9b12642

11 files changed

+303
-37
lines changed

doc/ovhcloud_cloud_loadbalancer_edit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ ovhcloud cloud loadbalancer edit <loadbalancer_id> [flags]
1111
```
1212
--description string Description of the loadbalancer
1313
--editor Use a text editor to define parameters
14+
--flavor string Flavor ID of the loadbalancer (can be retrieved with 'cloud reference loadbalancer list-flavors <region>')
1415
-h, --help help for edit
1516
--name string Name of the loadbalancer
16-
--size string Size of the load balancer (S, M, L)
1717
```
1818

1919
### Options inherited from parent commands

doc/ovhcloud_cloud_reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,6 @@ Fetch reference data in the given cloud project
3434
* [ovhcloud cloud reference database](ovhcloud_cloud_reference_database.md) - Fetch database reference data in the given cloud project
3535
* [ovhcloud cloud reference list-flavors](ovhcloud_cloud_reference_list-flavors.md) - List available flavors in the given cloud project
3636
* [ovhcloud cloud reference list-images](ovhcloud_cloud_reference_list-images.md) - List available images in the given cloud project
37+
* [ovhcloud cloud reference loadbalancer](ovhcloud_cloud_reference_loadbalancer.md) - Fetch loadbalancer reference data in the given cloud project
3738
* [ovhcloud cloud reference rancher](ovhcloud_cloud_reference_rancher.md) - Fetch Rancher reference data in the given cloud project
3839

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## ovhcloud cloud reference loadbalancer
2+
3+
Fetch loadbalancer reference data in the given cloud project
4+
5+
### Options
6+
7+
```
8+
-h, --help help for loadbalancer
9+
```
10+
11+
### Options inherited from parent commands
12+
13+
```
14+
--cloud-project string Cloud project ID
15+
-d, --debug Activate debug mode (will log all HTTP requests details)
16+
-f, --format string Output value according to given format (expression using https://github.com/PaesslerAG/gval syntax)
17+
Examples:
18+
--format 'id' (to extract a single field)
19+
--format 'nested.field.subfield' (to extract a nested field)
20+
--format '[id, 'name']' (to extract multiple fields as an array)
21+
--format '{"newKey": oldKey, "otherKey": nested.field}' (to extract and rename fields in an object)
22+
--format 'name+","+type' (to extract and concatenate fields in a string)
23+
--format '(nbFieldA + nbFieldB) * 10' (to compute values from numeric fields)
24+
-e, --ignore-errors Ignore errors in API calls when it is not fatal to the execution
25+
-i, --interactive Interactive output
26+
-j, --json Output in JSON
27+
-y, --yaml Output in YAML
28+
```
29+
30+
### SEE ALSO
31+
32+
* [ovhcloud cloud reference](ovhcloud_cloud_reference.md) - Fetch reference data in the given cloud project
33+
* [ovhcloud cloud reference loadbalancer list-flavors](ovhcloud_cloud_reference_loadbalancer_list-flavors.md) - List available loadbalancer flavors in the given cloud project
34+
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
## ovhcloud cloud reference loadbalancer list-flavors
2+
3+
List available loadbalancer flavors in the given cloud project
4+
5+
```
6+
ovhcloud cloud reference loadbalancer list-flavors <region (GRA9, BHS5, ...)> [flags]
7+
```
8+
9+
### Options
10+
11+
```
12+
--filter stringArray Filter results by any property using https://github.com/PaesslerAG/gval syntax
13+
Examples:
14+
--filter 'state="running"'
15+
--filter 'name=~"^my.*"'
16+
--filter 'nested.property.subproperty>10'
17+
--filter 'startDate>="2023-12-01"'
18+
--filter 'name=~"something" && nbField>10'
19+
-h, --help help for list-flavors
20+
```
21+
22+
### Options inherited from parent commands
23+
24+
```
25+
--cloud-project string Cloud project ID
26+
-d, --debug Activate debug mode (will log all HTTP requests details)
27+
-f, --format string Output value according to given format (expression using https://github.com/PaesslerAG/gval syntax)
28+
Examples:
29+
--format 'id' (to extract a single field)
30+
--format 'nested.field.subfield' (to extract a nested field)
31+
--format '[id, 'name']' (to extract multiple fields as an array)
32+
--format '{"newKey": oldKey, "otherKey": nested.field}' (to extract and rename fields in an object)
33+
--format 'name+","+type' (to extract and concatenate fields in a string)
34+
--format '(nbFieldA + nbFieldB) * 10' (to compute values from numeric fields)
35+
-e, --ignore-errors Ignore errors in API calls when it is not fatal to the execution
36+
-i, --interactive Interactive output
37+
-j, --json Output in JSON
38+
-y, --yaml Output in YAML
39+
```
40+
41+
### SEE ALSO
42+
43+
* [ovhcloud cloud reference loadbalancer](ovhcloud_cloud_reference_loadbalancer.md) - Fetch loadbalancer reference data in the given cloud project
44+

internal/cmd/cloud_loadbalancer.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ func initCloudLoadbalancerCommand(cloudCmd *cobra.Command) {
3737
Run: cloud.EditCloudLoadbalancer,
3838
Args: cobra.ExactArgs(1),
3939
}
40-
editLoadbalancerCmd.Flags().StringVar(&cloud.CloudLoadbalancerUpdateFields.Name, "name", "", "Name of the loadbalancer")
41-
editLoadbalancerCmd.Flags().StringVar(&cloud.CloudLoadbalancerUpdateFields.Description, "description", "", "Description of the loadbalancer")
42-
editLoadbalancerCmd.Flags().StringVar(&cloud.CloudLoadbalancerUpdateFields.Size, "size", "", "Size of the load balancer (S, M, L)")
40+
editLoadbalancerCmd.Flags().StringVar(&cloud.CloudLoadbalancerUpdateSpec.Name, "name", "", "Name of the loadbalancer")
41+
editLoadbalancerCmd.Flags().StringVar(&cloud.CloudLoadbalancerUpdateSpec.Description, "description", "", "Description of the loadbalancer")
42+
editLoadbalancerCmd.Flags().StringVar(&cloud.CloudLoadbalancerUpdateSpec.FlavorId, "flavor", "", "Flavor ID of the loadbalancer (can be retrieved with 'cloud reference loadbalancer list-flavors <region>')")
4343
addInteractiveEditorFlag(editLoadbalancerCmd)
4444
loadbalancerCmd.AddCommand(editLoadbalancerCmd)
4545

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// SPDX-FileCopyrightText: 2025 OVH SAS <opensource@ovh.net>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package cmd_test
6+
7+
import (
8+
"net/http"
9+
10+
"github.com/jarcoal/httpmock"
11+
"github.com/maxatome/go-testdeep/td"
12+
"github.com/ovh/ovhcloud-cli/internal/cmd"
13+
)
14+
15+
func (ms *MockSuite) TestCloudLoadbalancerGetCmd(assert, require *td.T) {
16+
httpmock.RegisterResponder(http.MethodGet, "https://eu.api.ovh.com/1.0/cloud/project/fakeProjectID/region",
17+
httpmock.NewStringResponder(200, `["GRA11", "SBG5", "BHS5"]`))
18+
19+
httpmock.RegisterResponder(http.MethodGet, "https://eu.api.ovh.com/1.0/cloud/project/fakeProjectID/region/GRA11",
20+
httpmock.NewStringResponder(200, `{
21+
"name": "GRA11",
22+
"type": "region",
23+
"status": "UP",
24+
"services": [
25+
{
26+
"name": "octavialoadbalancer",
27+
"status": "UP"
28+
}
29+
],
30+
"countryCode": "fr",
31+
"ipCountries": [],
32+
"continentCode": "NA",
33+
"availabilityZones": [],
34+
"datacenterLocation": "GRA11"
35+
}`))
36+
37+
httpmock.RegisterResponder(http.MethodGet, "https://eu.api.ovh.com/1.0/cloud/project/fakeProjectID/region/SBG5",
38+
httpmock.NewStringResponder(200, `{
39+
"name": "SBG5",
40+
"type": "region",
41+
"status": "UP",
42+
"services": [
43+
{
44+
"name": "octavialoadbalancer",
45+
"status": "UP"
46+
}
47+
],
48+
"countryCode": "fr",
49+
"ipCountries": [],
50+
"continentCode": "NA",
51+
"availabilityZones": [],
52+
"datacenterLocation": "SBG5"
53+
}`))
54+
55+
httpmock.RegisterResponder(http.MethodGet, "https://eu.api.ovh.com/1.0/cloud/project/fakeProjectID/region/BHS5",
56+
httpmock.NewStringResponder(200, `{
57+
"name": "BHS5",
58+
"type": "region",
59+
"status": "UP",
60+
"services": [],
61+
"countryCode": "ca",
62+
"ipCountries": [],
63+
"continentCode": "NA",
64+
"availabilityZones": [],
65+
"datacenterLocation": "BHS5"
66+
}`))
67+
68+
httpmock.RegisterResponder(http.MethodGet,
69+
"https://eu.api.ovh.com/1.0/cloud/project/fakeProjectID/region/SBG5/loadbalancing/loadbalancer/fakeLB",
70+
httpmock.NewStringResponder(200, `{
71+
"createdAt": "2024-07-30T08:26:51Z",
72+
"flavorId": "f862fa22-6275-4f8f-885e-66a8faf5e44e",
73+
"floatingIp": null,
74+
"id": "334fc97e-a8db-11f0-944d-0050568ce122",
75+
"name": "loadbalancer-sbg5-2024-07-30",
76+
"operatingStatus": "online",
77+
"provisioningStatus": "active",
78+
"region": "SBG5",
79+
"updatedAt": "2025-10-14T08:48:33Z",
80+
"vipAddress": "1.2.3.4",
81+
"vipNetworkId": "3f29f530-a8db-11f0-9ab2-0050568ce122",
82+
"vipSubnetId": "44a869c4-a8db-11f0-899f-0050568ce122"
83+
}`))
84+
85+
out, err := cmd.Execute("cloud", "loadbalancer", "get", "fakeLB", "--cloud-project", "fakeProjectID")
86+
require.CmpNoError(err)
87+
assert.Cmp(cleanWhitespacesHelper(out), `
88+
# 🚀 Load balancer fakeLB
89+
90+
*loadbalancer-sbg5-2024-07-30*
91+
92+
## General information
93+
94+
**Region**: SBG5
95+
**Operating status**: online
96+
**Provisioning status**: active
97+
**Flavor ID**: f862fa22-6275-4f8f-885e-66a8faf5e44e
98+
**Creation date**: 2024-07-30T08:26:51Z
99+
100+
## Technical information
101+
102+
**VIP address**: 1.2.3.4
103+
**VIP network ID**: 3f29f530-a8db-11f0-9ab2-0050568ce122
104+
**VIP subnet ID**: 44a869c4-a8db-11f0-899f-0050568ce122
105+
106+
💡 Use option --json or --yaml to get the raw output with all information
107+
108+
`)
109+
}

internal/cmd/cloud_reference.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,19 @@ func initCloudReferenceCmd(cloudCmd *cobra.Command) {
111111
Args: cobra.NoArgs,
112112
}))
113113

114+
// Loadbalancer reference commands
115+
loadbalancerReferenceCmd := &cobra.Command{
116+
Use: "loadbalancer",
117+
Short: "Fetch loadbalancer reference data in the given cloud project",
118+
}
119+
referenceCmd.AddCommand(loadbalancerReferenceCmd)
120+
121+
loadbalancerReferenceCmd.AddCommand(withFilterFlag(&cobra.Command{
122+
Use: "list-flavors <region (GRA9, BHS5, ...)>",
123+
Short: "List available loadbalancer flavors in the given cloud project",
124+
Run: cloud.ListLoadbalancerFlavors,
125+
Args: cobra.ExactArgs(1),
126+
}))
127+
114128
cloudCmd.AddCommand(referenceCmd)
115129
}

internal/cmd/cmd_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package cmd_test
66

77
import (
88
"os"
9+
"regexp"
910
"testing"
1011

1112
"github.com/jarcoal/httpmock"
@@ -75,3 +76,7 @@ func resetSubCommandFlagValues(root *cobra.Command) {
7576
resetSubCommandFlagValues(c)
7677
}
7778
}
79+
80+
func cleanWhitespacesHelper(s string) string {
81+
return regexp.MustCompile(` +\n`).ReplaceAllString(s, "\n")
82+
}

internal/services/cloud/cloud_loadbalancer.go

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,83 @@ import (
1111

1212
"github.com/ovh/ovhcloud-cli/internal/assets"
1313
"github.com/ovh/ovhcloud-cli/internal/display"
14+
filtersLib "github.com/ovh/ovhcloud-cli/internal/filters"
1415
"github.com/ovh/ovhcloud-cli/internal/flags"
16+
httpLib "github.com/ovh/ovhcloud-cli/internal/http"
1517
"github.com/ovh/ovhcloud-cli/internal/services/common"
1618
"github.com/spf13/cobra"
1719
)
1820

1921
var (
20-
cloudprojectLoadbalancerColumnsToDisplay = []string{"id", "openstackRegion", "size", "status"}
22+
cloudprojectLoadbalancerColumnsToDisplay = []string{"id", "name", "region", "provisioningStatus", "operatingStatus"}
2123

2224
//go:embed templates/cloud_loadbalancer.tmpl
2325
cloudLoadbalancerTemplate string
2426

25-
CloudLoadbalancerUpdateFields struct {
27+
CloudLoadbalancerUpdateSpec struct {
2628
Description string `json:"description,omitempty"`
2729
Name string `json:"name,omitempty"`
28-
Size string `json:"size,omitempty"`
30+
FlavorId string `json:"flavorId,omitempty"`
2931
}
3032
)
3133

34+
func locateLoadbalancer(projectID, loadbalancerID string) (string, map[string]any, error) {
35+
// Fetch regions with loadbalancer feature available
36+
regions, err := getCloudRegionsWithFeatureAvailable(projectID, "octavialoadbalancer")
37+
if err != nil {
38+
return "", nil, fmt.Errorf("failed to fetch regions with loadbalancer feature available: %w", err)
39+
}
40+
41+
// Search for the given loadbalancer in all regions
42+
for _, region := range regions {
43+
endpoint := fmt.Sprintf("/cloud/project/%s/region/%s/loadbalancing/loadbalancer/%s",
44+
projectID, url.PathEscape(region.(string)), url.PathEscape(loadbalancerID))
45+
46+
var loadbalancer map[string]any
47+
if err := httpLib.Client.Get(endpoint, &loadbalancer); err == nil {
48+
return region.(string), loadbalancer, nil
49+
}
50+
}
51+
52+
return "", nil, fmt.Errorf("no loadbalancer found with id %s", loadbalancerID)
53+
}
54+
3255
func ListCloudLoadbalancers(_ *cobra.Command, _ []string) {
3356
projectID, err := getConfiguredCloudProject()
3457
if err != nil {
3558
display.OutputError(&flags.OutputFormatConfig, "%s", err)
3659
return
3760
}
3861

39-
common.ManageListRequest(fmt.Sprintf("/cloud/project/%s/loadbalancer", projectID), "", cloudprojectLoadbalancerColumnsToDisplay, flags.GenericFilters)
62+
// Fetch regions with loadbalancer feature available
63+
regions, err := getCloudRegionsWithFeatureAvailable(projectID, "octavialoadbalancer")
64+
if err != nil {
65+
display.OutputError(&flags.OutputFormatConfig, "failed to fetch regions with loadbalancer feature available: %s", err)
66+
return
67+
}
68+
69+
// Fetch loadbalancers in all regions
70+
endpoint := fmt.Sprintf("/cloud/project/%s/region", projectID)
71+
containers, err := httpLib.FetchObjectsParallel[[]map[string]any](endpoint+"/%s/loadbalancing/loadbalancer", regions, true)
72+
if err != nil {
73+
display.OutputError(&flags.OutputFormatConfig, "failed to fetch loadbalancers: %s", err)
74+
return
75+
}
76+
77+
// Flatten loadbalancers in a single array
78+
var allLoadbalancers []map[string]any
79+
for _, regionLoadbalancers := range containers {
80+
allLoadbalancers = append(allLoadbalancers, regionLoadbalancers...)
81+
}
82+
83+
// Filter results
84+
allLoadbalancers, err = filtersLib.FilterLines(allLoadbalancers, flags.GenericFilters)
85+
if err != nil {
86+
display.OutputError(&flags.OutputFormatConfig, "failed to filter results: %s", err)
87+
return
88+
}
89+
90+
display.RenderTable(allLoadbalancers, cloudprojectLoadbalancerColumnsToDisplay, &flags.OutputFormatConfig)
4091
}
4192

4293
func GetCloudLoadbalancer(_ *cobra.Command, args []string) {
@@ -46,7 +97,13 @@ func GetCloudLoadbalancer(_ *cobra.Command, args []string) {
4697
return
4798
}
4899

49-
common.ManageObjectRequest(fmt.Sprintf("/cloud/project/%s/loadbalancer", projectID), args[0], cloudLoadbalancerTemplate)
100+
_, lb, err := locateLoadbalancer(projectID, args[0])
101+
if err != nil {
102+
display.OutputError(&flags.OutputFormatConfig, "%s", err)
103+
return
104+
}
105+
106+
display.OutputObject(lb, args[0], cloudLoadbalancerTemplate, &flags.OutputFormatConfig)
50107
}
51108

52109
func EditCloudLoadbalancer(cmd *cobra.Command, args []string) {
@@ -56,11 +113,17 @@ func EditCloudLoadbalancer(cmd *cobra.Command, args []string) {
56113
return
57114
}
58115

116+
region, _, err := locateLoadbalancer(projectID, args[0])
117+
if err != nil {
118+
display.OutputError(&flags.OutputFormatConfig, "%s", err)
119+
return
120+
}
121+
59122
if err := common.EditResource(
60123
cmd,
61-
"/cloud/project/{serviceName}/loadbalancer/{loadBalancerId}",
62-
fmt.Sprintf("/cloud/project/%s/loadbalancer/%s", projectID, url.PathEscape(args[0])),
63-
CloudLoadbalancerUpdateFields,
124+
"/cloud/project/{serviceName}/region/{regionName}/loadbalancing/loadbalancer/{loadBalancerId}",
125+
fmt.Sprintf("/cloud/project/%s/region/%s/loadbalancing/loadbalancer/%s", projectID, url.PathEscape(region), url.PathEscape(args[0])),
126+
CloudLoadbalancerUpdateSpec,
64127
assets.CloudOpenapiSchema,
65128
); err != nil {
66129
display.OutputError(&flags.OutputFormatConfig, "%s", err)

internal/services/cloud/cloud_reference.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,14 @@ func ListDatabaseEngines(_ *cobra.Command, _ []string) {
230230

231231
display.RenderTable(engines, []string{"name", "description", "category", "versions", "defaultVersion"}, &flags.OutputFormatConfig)
232232
}
233+
234+
func ListLoadbalancerFlavors(cmd *cobra.Command, args []string) {
235+
projectID, err := getConfiguredCloudProject()
236+
if err != nil {
237+
display.OutputError(&flags.OutputFormatConfig, "%s", err)
238+
return
239+
}
240+
241+
endpoint := fmt.Sprintf("/cloud/project/%s/region/%s/loadbalancing/flavor", projectID, url.PathEscape(args[0]))
242+
common.ManageListRequestNoExpand(endpoint, []string{"id", "name", "region"}, flags.GenericFilters)
243+
}

0 commit comments

Comments
 (0)