Skip to content

Commit d5aaecb

Browse files
committed
feat: Add subnet+gateway information in output when creating a private network
Signed-off-by: Arthur Amstutz <arthur.amstutz@corp.ovh.com>
1 parent 9588435 commit d5aaecb

File tree

3 files changed

+232
-13
lines changed

3 files changed

+232
-13
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ You can also run the CLI using Docker:
4949
docker run -it --rm -v ovhcloud-cli-config-files:/config ovhcom/ovhcloud-cli login
5050
```
5151

52-
## Install using HomeBrew
52+
## Install using Homebrew
5353

5454
```sh
5555
brew install ovh/tap/ovhcloud-cli

internal/cmd/cloud_network_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
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/maxatome/tdhttpmock"
13+
"github.com/ovh/ovhcloud-cli/internal/cmd"
14+
)
15+
16+
func (ms *MockSuite) TestCloudPrivateNetworkCreateCmd(assert, require *td.T) {
17+
httpmock.RegisterMatcherResponder(http.MethodPost,
18+
"https://eu.api.ovh.com/1.0/cloud/project/fakeProjectID/region/BHS5/network",
19+
tdhttpmock.JSONBody(td.JSON(`
20+
{
21+
"gateway": {
22+
"model": "s",
23+
"name": "TestFromTheCLI"
24+
},
25+
"name": "TestFromTheCLI",
26+
"subnet": {
27+
"cidr": "10.0.0.2/24",
28+
"enableDhcp": false,
29+
"enableGatewayIp": true,
30+
"ipVersion": 4
31+
}
32+
}`),
33+
),
34+
httpmock.NewStringResponder(200, `{"id": "operation-12345"}`),
35+
)
36+
37+
httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/1.0/cloud/project/fakeProjectID/operation/operation-12345",
38+
httpmock.NewStringResponder(200, `
39+
{
40+
"id": "6610ec10-9b09-11f0-a8ac-0050568ce122",
41+
"action": "network#create",
42+
"createdAt": "2025-09-26T20:43:14.376907+02:00",
43+
"startedAt": "2025-09-26T20:43:14.376907+02:00",
44+
"completedAt": "2025-09-26T20:43:36.631086+02:00",
45+
"progress": 0,
46+
"regions": [
47+
"BHS5"
48+
],
49+
"resourceId": "80c1de3e-9b09-11f0-993b-0050568ce122",
50+
"status": "completed",
51+
"subOperations": [
52+
{
53+
"id": "8c0806ba-9b09-11f0-9a54-0050568ce122",
54+
"action": "gateway#create",
55+
"startedAt": "2025-09-26T20:43:14.376907+02:00",
56+
"completedAt": "2025-09-26T20:43:36.631086+02:00",
57+
"progress": 0,
58+
"regions": [
59+
"BHS5"
60+
],
61+
"resourceId": "97a2703c-9b09-11f0-9b6c-0050568ce122",
62+
"status": "completed"
63+
}
64+
]
65+
}`),
66+
)
67+
68+
httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/1.0/cloud/project/fakeProjectID/network/private",
69+
httpmock.NewStringResponder(200, `[
70+
{
71+
"id": "pn-example",
72+
"name": "TestFromTheCLI",
73+
"vlanId": 1234,
74+
"regions": [
75+
{
76+
"region": "BHS5",
77+
"status": "ACTIVE",
78+
"openstackId": "80c1de3e-9b09-11f0-993b-0050568ce122"
79+
}
80+
],
81+
"type": "private",
82+
"status": "ACTIVE"
83+
}
84+
]`),
85+
)
86+
87+
httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/1.0/cloud/project/fakeProjectID/region/BHS5/network/80c1de3e-9b09-11f0-993b-0050568ce122/subnet",
88+
httpmock.NewStringResponder(200, `[
89+
{
90+
"id": "c59a3fdc-9b0f-11f0-ac97-0050568ce122",
91+
"name": "TestFromTheCLI",
92+
"cidr": "10.0.0.0/24",
93+
"ipVersion": 4,
94+
"dhcpEnabled": false,
95+
"gatewayIp": "10.0.0.1",
96+
"allocationPools": [
97+
{
98+
"start": "10.0.0.2",
99+
"end": "10.0.0.254"
100+
}
101+
]
102+
}
103+
]`),
104+
)
105+
106+
httpmock.RegisterResponder("GET", "https://eu.api.ovh.com/1.0/cloud/project/fakeProjectID/region/BHS5/gateway?subnetId=c59a3fdc-9b0f-11f0-ac97-0050568ce122",
107+
httpmock.NewStringResponder(200, `[
108+
{
109+
"id": "e7045f34-8f2b-41a4-a734-97b7b0e323de",
110+
"status": "active",
111+
"name": "TestFromTheCLI",
112+
"interfaces": [
113+
{
114+
"id": "56d17852-9b11-11f0-8d13-0050568ce122",
115+
"ip": "10.0.0.1",
116+
"subnetId": "56d17852-9b11-11f0-8d13-0050568ce122",
117+
"networkId": "c59a3fdc-9b0f-11f0-ac97-0050568ce122"
118+
},
119+
{
120+
"id": "56d17852-9b11-11f0-8d13-0050568ce122",
121+
"ip": "10.0.0.218",
122+
"subnetId": "56d17852-9b11-11f0-8d13-0050568ce122",
123+
"networkId": "c59a3fdc-9b0f-11f0-ac97-0050568ce122"
124+
}
125+
],
126+
"externalInformation": {
127+
"ips": [
128+
{
129+
"ip": "1.2.3.4",
130+
"subnetId": "981c226c-57da-4766-966b-3b45db0cfc84"
131+
}
132+
],
133+
"networkId": "c59a3fdc-9b0f-11f0-ac97-0050568ce122"
134+
},
135+
"region": "BHS5",
136+
"model": "s"
137+
}
138+
]`),
139+
)
140+
141+
out, err := cmd.Execute("cloud", "network", "private", "create", "BHS5", "--cloud-project", "fakeProjectID",
142+
"--gateway-model", "s", "--gateway-name", "TestFromTheCLI", "--name", "TestFromTheCLI", "--subnet-cidr",
143+
"10.0.0.2/24", "--subnet-ip-version", "4", "--wait", "--subnet-enable-gateway-ip", "--yaml")
144+
require.CmpNoError(err)
145+
assert.String(out, `details:
146+
id: pn-example
147+
openstackId: 80c1de3e-9b09-11f0-993b-0050568ce122
148+
region: BHS5
149+
subnets:
150+
- gateways:
151+
- id: e7045f34-8f2b-41a4-a734-97b7b0e323de
152+
name: TestFromTheCLI
153+
id: c59a3fdc-9b0f-11f0-ac97-0050568ce122
154+
name: TestFromTheCLI
155+
message: '✅ Network pn-example created successfully (Openstack ID: 80c1de3e-9b09-11f0-993b-0050568ce122)'
156+
`)
157+
}

internal/services/cloud/cloud_network.go

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ var (
8080
Subnet struct {
8181
Name string `json:"name,omitempty"`
8282
Cidr string `json:"cidr,omitempty"`
83-
EnableDhcp bool `json:"enableDhcp,omitempty"`
84-
EnableGatewayIp bool `json:"enableGatewayIp,omitempty"`
83+
EnableDhcp bool `json:"enableDhcp"`
84+
EnableGatewayIp bool `json:"enableGatewayIp"`
8585
GatewayIp string `json:"gatewayIp,omitempty"`
8686
DnsNameServers []string `json:"dnsNameServers,omitempty"`
8787
UseDefaultPublicDNSResolver bool `json:"useDefaultPublicDNSResolver,omitempty"`
@@ -115,6 +115,16 @@ type (
115115
Destination string `json:"destination,omitempty"`
116116
NextHop string `json:"nextHop,omitempty"`
117117
}
118+
119+
NetworkRegionDetails struct {
120+
OpenstackID string `json:"openstackId"`
121+
Region string `json:"region"`
122+
}
123+
124+
PrivateNetwork struct {
125+
ID string `json:"id"`
126+
Regions []NetworkRegionDetails `json:"regions"`
127+
}
118128
)
119129

120130
func ListPrivateNetworks(_ *cobra.Command, _ []string) {
@@ -283,29 +293,81 @@ You can check the status of the operation with: 'ovhcloud cloud operation get %[
283293
}
284294

285295
// Fetch all private networks
286-
var networks []struct {
287-
ID string `json:"id"`
288-
Regions []struct {
289-
OpenstackID string `json:"openstackId"`
290-
Region string `json:"region"`
291-
} `json:"regions"`
292-
}
296+
var networks []PrivateNetwork
293297
if err := httpLib.Client.Get(fmt.Sprintf("/cloud/project/%s/network/private", projectID), &networks); err != nil {
294298
display.OutputError(&flags.OutputFormatConfig, "failed to fetch private networks: %s", err)
295299
return
296300
}
297301

298302
// Find the created network
303+
var (
304+
foundNetwork *PrivateNetwork
305+
foundRegionNetwork *NetworkRegionDetails
306+
)
307+
308+
eachNetwork:
299309
for _, network := range networks {
300310
for _, regionDetails := range network.Regions {
301311
if regionDetails.OpenstackID == networkID && regionDetails.Region == region {
302-
display.OutputInfo(&flags.OutputFormatConfig, regionDetails, "✅ Network %s created successfully (Openstack ID: %s)", network.ID, regionDetails.OpenstackID)
303-
return
312+
foundNetwork = &network
313+
foundRegionNetwork = &regionDetails
314+
break eachNetwork
315+
}
316+
}
317+
}
318+
319+
if foundNetwork == nil {
320+
display.OutputError(&flags.OutputFormatConfig, "created network not found, this is unexpected")
321+
return
322+
}
323+
324+
// Fetch subnets of created network
325+
endpoint = fmt.Sprintf("/cloud/project/%s/region/%s/network/%s/subnet", projectID, url.PathEscape(region), url.PathEscape(foundRegionNetwork.OpenstackID))
326+
var subnets []map[string]any
327+
if err := httpLib.Client.Get(endpoint, &subnets); err != nil {
328+
display.OutputError(&flags.OutputFormatConfig, "failed to fetch subnets of created network: %s", err)
329+
return
330+
}
331+
332+
// Fetch gateway of created subnets and prepare output
333+
var outputSubnets []map[string]any
334+
for _, subnet := range subnets {
335+
endpoint = fmt.Sprintf("/cloud/project/%s/region/%s/gateway?subnetId=%s",
336+
projectID,
337+
url.PathEscape(region),
338+
url.PathEscape(subnet["id"].(string)),
339+
)
340+
341+
var gateways []map[string]any
342+
if err := httpLib.Client.Get(endpoint, &gateways); err != nil {
343+
display.OutputError(&flags.OutputFormatConfig, "failed to fetch gateways of created network: %s", err)
344+
return
345+
}
346+
347+
var outputGateways []map[string]any
348+
for _, gateway := range gateways {
349+
outputGateway := map[string]any{
350+
"id": gateway["id"],
351+
"name": gateway["name"],
304352
}
353+
outputGateways = append(outputGateways, outputGateway)
305354
}
355+
356+
outputSubnets = append(outputSubnets, map[string]any{
357+
"id": subnet["id"],
358+
"name": subnet["name"],
359+
"gateways": outputGateways,
360+
})
361+
}
362+
363+
networkObject := map[string]any{
364+
"id": foundNetwork.ID,
365+
"openstackId": foundRegionNetwork.OpenstackID,
366+
"region": foundRegionNetwork.Region,
367+
"subnets": outputSubnets,
306368
}
307369

308-
display.OutputError(&flags.OutputFormatConfig, "created network not found, this is unexpected")
370+
display.OutputInfo(&flags.OutputFormatConfig, networkObject, "✅ Network %s created successfully (Openstack ID: %s)", foundNetwork.ID, foundRegionNetwork.OpenstackID)
309371
}
310372

311373
func DeletePrivateNetwork(_ *cobra.Command, args []string) {

0 commit comments

Comments
 (0)