Skip to content

Commit 0663765

Browse files
authored
chore: add utility function to generate URL (#182)
Signed-off-by: Dmitry Shmulevich <[email protected]>
1 parent ad3d94a commit 0663765

File tree

4 files changed

+114
-120
lines changed

4 files changed

+114
-120
lines changed

internal/httpreq/httpreq.go

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,6 @@
11
/*
2-
* Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
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.
2+
* Copyright 2024-2025 NVIDIA CORPORATION
3+
* SPDX-License-Identifier: Apache-2.0
154
*/
165

176
package httpreq
@@ -22,9 +11,13 @@ import (
2211
"io"
2312
"math"
2413
"net/http"
14+
"net/url"
15+
"path"
2516
"time"
2617

2718
"k8s.io/klog/v2"
19+
20+
"github.com/NVIDIA/topograph/internal/httperr"
2821
)
2922

3023
var (
@@ -44,10 +37,10 @@ var (
4437
type RequestFunc func() (*http.Request, error)
4538

4639
// DoRequest sends HTTP requests and returns HTTP response
47-
func DoRequest(f RequestFunc, insecureSkipVerify bool) (*http.Response, []byte, error) {
40+
func DoRequest(f RequestFunc, insecureSkipVerify bool) (*http.Response, []byte, *httperr.Error) {
4841
req, err := f()
4942
if err != nil {
50-
return nil, nil, err
43+
return nil, nil, httperr.NewError(http.StatusInternalServerError, err.Error())
5144
}
5245
klog.V(4).Infof("Sending HTTP request %s", req.URL.String())
5346
client := &http.Client{}
@@ -58,24 +51,28 @@ func DoRequest(f RequestFunc, insecureSkipVerify bool) (*http.Response, []byte,
5851
}
5952
resp, err := client.Do(req)
6053
if err != nil {
61-
return nil, nil, fmt.Errorf("failed to send HTTP request: %v", err)
54+
code := http.StatusBadGateway
55+
if resp != nil {
56+
code = resp.StatusCode
57+
}
58+
return resp, nil, httperr.NewError(code, err.Error())
6259
}
6360
defer func() { _ = resp.Body.Close() }()
6461

6562
body, err := io.ReadAll(resp.Body)
6663
if err != nil {
67-
return nil, nil, fmt.Errorf("failed to read HTTP response: %v", err)
64+
return resp, nil, httperr.NewError(http.StatusInternalServerError, fmt.Sprintf("failed to read HTTP response: %v", err))
6865
}
6966

7067
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
7168
return resp, body, nil
7269
}
7370

74-
return resp, body, fmt.Errorf("%s: %s", resp.Status, string(body))
71+
return resp, body, httperr.NewError(resp.StatusCode, string(body))
7572
}
7673

7774
// DoRequestWithRetries sends HTTP requests and returns HTTP response; retries if needed
78-
func DoRequestWithRetries(f RequestFunc, insecureSkipVerify bool) (resp *http.Response, body []byte, err error) {
75+
func DoRequestWithRetries(f RequestFunc, insecureSkipVerify bool) (resp *http.Response, body []byte, err *httperr.Error) {
7976
klog.V(4).Infof("Sending HTTP request with retries")
8077
for r := 1; r <= retries; r++ {
8178
resp, body, err = DoRequest(f, insecureSkipVerify)
@@ -89,3 +86,22 @@ func DoRequestWithRetries(f RequestFunc, insecureSkipVerify bool) (resp *http.Re
8986

9087
return
9188
}
89+
90+
func GetURL(baseURL string, query map[string]string, paths ...string) (string, *httperr.Error) {
91+
u, err := url.Parse(baseURL)
92+
if err != nil {
93+
return "", httperr.NewError(http.StatusBadRequest, err.Error())
94+
}
95+
96+
u.Path = path.Join(append([]string{u.Path}, paths...)...)
97+
98+
if len(query) != 0 {
99+
q := u.Query()
100+
for key, val := range query {
101+
q.Set(key, val)
102+
}
103+
u.RawQuery = q.Encode()
104+
}
105+
106+
return u.String(), nil
107+
}

internal/httpreq/httpreq_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2025 NVIDIA CORPORATION
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package httpreq
7+
8+
import (
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestGetURL(t *testing.T) {
15+
testCases := []struct {
16+
name string
17+
baseURL string
18+
paths []string
19+
query map[string]string
20+
url string
21+
err string
22+
}{
23+
{
24+
name: "Case 1: bad base URL",
25+
baseURL: "123:",
26+
err: `parse "123:": first path segment in URL cannot contain colon`,
27+
},
28+
{
29+
name: "Case 2: single base URL",
30+
baseURL: "http://localhost",
31+
url: "http://localhost",
32+
},
33+
{
34+
name: "Case 3: base URL with path",
35+
baseURL: "http://localhost/",
36+
paths: []string{"a", "b/", "/c", "d/"},
37+
url: "http://localhost/a/b/c/d",
38+
},
39+
{
40+
name: "Case 4: base URL with path and query",
41+
baseURL: "http://localhost/",
42+
paths: []string{"a", "b/", "/c", "d/"},
43+
query: map[string]string{"key1": "val1", "key2": "val2"},
44+
url: "http://localhost/a/b/c/d?key1=val1&key2=val2",
45+
},
46+
}
47+
48+
for _, tc := range testCases {
49+
t.Run(tc.name, func(t *testing.T) {
50+
u, err := GetURL(tc.baseURL, tc.query, tc.paths...)
51+
if len(tc.err) != 0 {
52+
require.EqualError(t, err, tc.err)
53+
} else {
54+
55+
require.Nil(t, err)
56+
require.Equal(t, tc.url, u)
57+
}
58+
})
59+
}
60+
}

pkg/providers/netq/netq.go

Lines changed: 19 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import (
1111
"fmt"
1212
"io"
1313
"net/http"
14-
"net/url"
15-
"path"
1614
"strings"
1715

1816
"k8s.io/klog/v2"
@@ -58,19 +56,15 @@ func (p *Provider) generateTopologyConfig(ctx context.Context, cis []topology.Co
5856
"Content-Type": "application/json",
5957
"Accept": "application/json",
6058
}
61-
u, httpErr := getURL(p.params.ApiURL, nil, LoginURL)
59+
url, httpErr := httpreq.GetURL(p.params.ApiURL, nil, LoginURL)
6260
if httpErr != nil {
6361
return nil, httpErr
6462
}
65-
klog.V(4).Infof("Fetching %s", u)
66-
f := getRequestFunc(ctx, "POST", u, headers, payload)
67-
resp, data, err := httpreq.DoRequest(f, true)
68-
if err != nil {
69-
code := http.StatusInternalServerError
70-
if resp != nil {
71-
code = resp.StatusCode
72-
}
73-
return nil, httperr.NewError(code, err.Error())
63+
klog.V(4).Infof("Fetching %s", url)
64+
f := getRequestFunc(ctx, "POST", url, headers, payload)
65+
_, data, httpErr := httpreq.DoRequest(f, true)
66+
if httpErr != nil {
67+
return nil, httpErr
7468
}
7569

7670
if len(data) == 0 {
@@ -87,19 +81,15 @@ func (p *Provider) generateTopologyConfig(ctx context.Context, cis []topology.Co
8781
headers = map[string]string{
8882
"Authorization": "Bearer " + authOutput.AccessToken,
8983
}
90-
u, httpErr = getURL(p.params.ApiURL, nil, OpIdURL, p.params.OpID)
84+
url, httpErr = httpreq.GetURL(p.params.ApiURL, nil, OpIdURL, p.params.OpID)
9185
if httpErr != nil {
9286
return nil, httpErr
9387
}
94-
klog.V(4).Infof("Fetching %s", u)
95-
f = getRequestFunc(ctx, "GET", u, headers, nil)
96-
resp, data, err = httpreq.DoRequest(f, true)
97-
if err != nil {
98-
code := http.StatusInternalServerError
99-
if resp != nil {
100-
code = resp.StatusCode
101-
}
102-
return nil, httperr.NewError(code, err.Error())
88+
klog.V(4).Infof("Fetching %s", url)
89+
f = getRequestFunc(ctx, "GET", url, headers, nil)
90+
_, data, httpErr = httpreq.DoRequest(f, true)
91+
if httpErr != nil {
92+
return nil, httpErr
10393
}
10494

10595
if len(data) == 0 {
@@ -118,24 +108,19 @@ func (p *Provider) generateTopologyConfig(ctx context.Context, cis []topology.Co
118108
"Authorization": "Bearer " + authOutput.AccessToken,
119109
}
120110
query := map[string]string{"timestamp": "0"}
121-
u, httpErr = getURL(p.params.ApiURL, query, TopologyURL)
111+
url, httpErr = httpreq.GetURL(p.params.ApiURL, query, TopologyURL)
122112
if httpErr != nil {
123113
return nil, httpErr
124114
}
125-
klog.V(4).Infof("Fetching %s", u)
126-
f = getRequestFunc(ctx, "POST", u, headers, payload)
127-
resp, data, err = httpreq.DoRequest(f, true)
128-
if err != nil {
129-
code := http.StatusInternalServerError
130-
if resp != nil {
131-
code = resp.StatusCode
132-
}
133-
return nil, httperr.NewError(code, err.Error())
115+
klog.V(4).Infof("Fetching %s", url)
116+
f = getRequestFunc(ctx, "POST", url, headers, payload)
117+
_, data, httpErr = httpreq.DoRequest(f, true)
118+
if httpErr != nil {
119+
return nil, httpErr
134120
}
135121

136122
var netqResponse []NetqResponse
137-
err = json.Unmarshal(data, &netqResponse)
138-
if err != nil {
123+
if err := json.Unmarshal(data, &netqResponse); err != nil {
139124
return nil, httperr.NewError(http.StatusBadGateway, fmt.Sprintf("netq output read failed: %v", err))
140125
}
141126

@@ -155,25 +140,6 @@ func getRequestFunc(ctx context.Context, method, url string, headers map[string]
155140
}
156141
}
157142

158-
func getURL(baseURL string, query map[string]string, paths ...string) (string, *httperr.Error) {
159-
u, err := url.Parse(baseURL)
160-
if err != nil {
161-
return "", httperr.NewError(http.StatusBadRequest, err.Error())
162-
}
163-
164-
u.Path = path.Join(append([]string{u.Path}, paths...)...)
165-
166-
if len(query) != 0 {
167-
q := u.Query()
168-
for key, val := range query {
169-
q.Set(key, val)
170-
}
171-
u.RawQuery = q.Encode()
172-
}
173-
174-
return u.String(), nil
175-
}
176-
177143
// parseNetq parses Netq topology output
178144
func parseNetq(resp []NetqResponse, inputNodes map[string]bool) (*topology.Vertex, *httperr.Error) {
179145
if len(resp) != 1 {

pkg/providers/netq/netq_test.go

Lines changed: 0 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -124,51 +124,3 @@ func TestParseNetq(t *testing.T) {
124124
_, err = parseNetq(netqResponse, map[string]bool{})
125125
require.EqualError(t, err, "invalid NetQ response: multiple entries")
126126
}
127-
128-
func TestGetURL(t *testing.T) {
129-
testCases := []struct {
130-
name string
131-
baseURL string
132-
paths []string
133-
query map[string]string
134-
url string
135-
err string
136-
}{
137-
{
138-
name: "Case 1: bad base URL",
139-
baseURL: "123:",
140-
err: `parse "123:": first path segment in URL cannot contain colon`,
141-
},
142-
{
143-
name: "Case 2: single base URL",
144-
baseURL: "http://localhost",
145-
url: "http://localhost",
146-
},
147-
{
148-
name: "Case 3: base URL with path",
149-
baseURL: "http://localhost/",
150-
paths: []string{"a", "b/", "/c", "d/"},
151-
url: "http://localhost/a/b/c/d",
152-
},
153-
{
154-
name: "Case 3: base URL with path and query",
155-
baseURL: "http://localhost/",
156-
paths: []string{"a", "b/", "/c", "d/"},
157-
query: map[string]string{"key1": "val1", "key2": "val2"},
158-
url: "http://localhost/a/b/c/d?key1=val1&key2=val2",
159-
},
160-
}
161-
162-
for _, tc := range testCases {
163-
t.Run(tc.name, func(t *testing.T) {
164-
u, err := getURL(tc.baseURL, tc.query, tc.paths...)
165-
if len(tc.err) != 0 {
166-
require.EqualError(t, err, tc.err)
167-
} else {
168-
169-
require.Nil(t, err)
170-
require.Equal(t, tc.url, u)
171-
}
172-
})
173-
}
174-
}

0 commit comments

Comments
 (0)