Skip to content

Commit b743a33

Browse files
authored
fix: allow getting resources with number as name (#571)
If the name of a user-named resource was numeric, it was impossible to `Get` it from its name, since a number would always be interpreted as an ID. This PR fixes this behavior on user-namable resources by continuing to check if a resource with the given `idOrName` exists even though the `idOrName` is numeric and no resource with the given ID was found. See hetznercloud/cli#874
1 parent 51baf8d commit b743a33

24 files changed

+584
-13
lines changed

hcloud/certificate.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,10 @@ func (c *CertificateClient) GetByName(ctx context.Context, name string) (*Certif
130130
// retrieves a Certificate by its name. If the Certificate does not exist, nil is returned.
131131
func (c *CertificateClient) Get(ctx context.Context, idOrName string) (*Certificate, *Response, error) {
132132
if id, err := strconv.ParseInt(idOrName, 10, 64); err == nil {
133-
return c.GetByID(ctx, id)
133+
cert, res, err := c.GetByID(ctx, id)
134+
if cert != nil || err != nil {
135+
return cert, res, err
136+
}
134137
}
135138
return c.GetByName(ctx, idOrName)
136139
}

hcloud/certificate_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,48 @@ func TestCertificateClientGetByName(t *testing.T) {
239239
})
240240
}
241241

242+
func TestCertificateClientGetByNumericName(t *testing.T) {
243+
env := newTestEnv()
244+
defer env.Teardown()
245+
246+
env.Mux.HandleFunc("/certificates/123", func(w http.ResponseWriter, r *http.Request) {
247+
w.Header().Set("Content-Type", "application/json")
248+
w.WriteHeader(http.StatusNotFound)
249+
json.NewEncoder(w).Encode(schema.ErrorResponse{
250+
Error: schema.Error{
251+
Code: string(ErrorCodeNotFound),
252+
},
253+
})
254+
})
255+
256+
env.Mux.HandleFunc("/certificates", func(w http.ResponseWriter, r *http.Request) {
257+
if r.URL.RawQuery != "name=123" {
258+
t.Fatal("missing name query")
259+
}
260+
json.NewEncoder(w).Encode(schema.CertificateListResponse{
261+
Certificates: []schema.Certificate{
262+
{
263+
ID: 1,
264+
Name: "123",
265+
},
266+
},
267+
})
268+
})
269+
270+
ctx := context.Background()
271+
272+
certficate, _, err := env.Client.Certificate.Get(ctx, "123")
273+
if err != nil {
274+
t.Fatal(err)
275+
}
276+
if certficate == nil {
277+
t.Fatal("no certficate")
278+
}
279+
if certficate.ID != 1 {
280+
t.Errorf("unexpected certficate ID: %v", certficate.ID)
281+
}
282+
}
283+
242284
func TestCertificateClientGetByNameNotFound(t *testing.T) {
243285
env := newTestEnv()
244286
defer env.Teardown()

hcloud/firewall.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,10 @@ func (c *FirewallClient) GetByName(ctx context.Context, name string) (*Firewall,
128128
// retrieves a Firewall by its name. If the Firewall does not exist, nil is returned.
129129
func (c *FirewallClient) Get(ctx context.Context, idOrName string) (*Firewall, *Response, error) {
130130
if id, err := strconv.ParseInt(idOrName, 10, 64); err == nil {
131-
return c.GetByID(ctx, id)
131+
fw, res, err := c.GetByID(ctx, id)
132+
if fw != nil || err != nil {
133+
return fw, res, err
134+
}
132135
}
133136
return c.GetByName(ctx, idOrName)
134137
}

hcloud/firewall_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,48 @@ func TestFirewallClientGetByName(t *testing.T) {
148148
})
149149
}
150150

151+
func TestFirewallClientGetByNumericName(t *testing.T) {
152+
env := newTestEnv()
153+
defer env.Teardown()
154+
155+
env.Mux.HandleFunc("/firewalls/123", func(w http.ResponseWriter, r *http.Request) {
156+
w.Header().Set("Content-Type", "application/json")
157+
w.WriteHeader(http.StatusNotFound)
158+
json.NewEncoder(w).Encode(schema.ErrorResponse{
159+
Error: schema.Error{
160+
Code: string(ErrorCodeNotFound),
161+
},
162+
})
163+
})
164+
165+
env.Mux.HandleFunc("/firewalls", func(w http.ResponseWriter, r *http.Request) {
166+
if r.URL.RawQuery != "name=123" {
167+
t.Fatal("missing name query")
168+
}
169+
json.NewEncoder(w).Encode(schema.FirewallListResponse{
170+
Firewalls: []schema.Firewall{
171+
{
172+
ID: 1,
173+
Name: "123",
174+
},
175+
},
176+
})
177+
})
178+
179+
ctx := context.Background()
180+
181+
firewall, _, err := env.Client.Firewall.Get(ctx, "123")
182+
if err != nil {
183+
t.Fatal(err)
184+
}
185+
if firewall == nil {
186+
t.Fatal("no firewall")
187+
}
188+
if firewall.ID != 1 {
189+
t.Errorf("unexpected firewall ID: %v", firewall.ID)
190+
}
191+
}
192+
151193
func TestFirewallClientGetByNameNotFound(t *testing.T) {
152194
env := newTestEnv()
153195
defer env.Teardown()

hcloud/floating_ip.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,10 @@ func (c *FloatingIPClient) GetByName(ctx context.Context, name string) (*Floatin
129129
// retrieves a Floating IP by its name. If the Floating IP does not exist, nil is returned.
130130
func (c *FloatingIPClient) Get(ctx context.Context, idOrName string) (*FloatingIP, *Response, error) {
131131
if id, err := strconv.ParseInt(idOrName, 10, 64); err == nil {
132-
return c.GetByID(ctx, id)
132+
ip, res, err := c.GetByID(ctx, id)
133+
if ip != nil || err != nil {
134+
return ip, res, err
135+
}
133136
}
134137
return c.GetByName(ctx, idOrName)
135138
}

hcloud/floating_ip_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,50 @@ func TestFloatingIPClientGetByName(t *testing.T) {
117117
})
118118
}
119119

120+
func TestFloatingIPClientGetByNumericName(t *testing.T) {
121+
env := newTestEnv()
122+
defer env.Teardown()
123+
124+
env.Mux.HandleFunc("/floating_ips/123", func(w http.ResponseWriter, r *http.Request) {
125+
w.Header().Set("Content-Type", "application/json")
126+
w.WriteHeader(http.StatusNotFound)
127+
json.NewEncoder(w).Encode(schema.ErrorResponse{
128+
Error: schema.Error{
129+
Code: string(ErrorCodeNotFound),
130+
},
131+
})
132+
})
133+
134+
env.Mux.HandleFunc("/floating_ips", func(w http.ResponseWriter, r *http.Request) {
135+
if r.URL.RawQuery != "name=123" {
136+
t.Fatal("missing name query")
137+
}
138+
json.NewEncoder(w).Encode(schema.FloatingIPListResponse{
139+
FloatingIPs: []schema.FloatingIP{
140+
{
141+
ID: 1,
142+
Name: "123",
143+
Type: "ipv4",
144+
IP: "131.232.99.1",
145+
},
146+
},
147+
})
148+
})
149+
150+
ctx := context.Background()
151+
152+
floatingIP, _, err := env.Client.FloatingIP.Get(ctx, "123")
153+
if err != nil {
154+
t.Fatal(err)
155+
}
156+
if floatingIP == nil {
157+
t.Fatal("no Floating IP")
158+
}
159+
if floatingIP.ID != 1 {
160+
t.Errorf("unexpected Floating IP ID: %v", floatingIP.ID)
161+
}
162+
}
163+
120164
func TestFloatingIPClientGetByNameNotFound(t *testing.T) {
121165
env := newTestEnv()
122166
defer env.Teardown()

hcloud/image.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ func (c *ImageClient) GetByNameAndArchitecture(ctx context.Context, name string,
134134
// Deprecated: Use [ImageClient.GetForArchitecture] instead.
135135
func (c *ImageClient) Get(ctx context.Context, idOrName string) (*Image, *Response, error) {
136136
if id, err := strconv.ParseInt(idOrName, 10, 64); err == nil {
137-
return c.GetByID(ctx, id)
137+
img, res, err := c.GetByID(ctx, id)
138+
if img != nil {
139+
return img, res, err
140+
}
138141
}
139142
return c.GetByName(ctx, idOrName)
140143
}
@@ -146,7 +149,10 @@ func (c *ImageClient) Get(ctx context.Context, idOrName string) (*Image, *Respon
146149
// check for this in your calling method.
147150
func (c *ImageClient) GetForArchitecture(ctx context.Context, idOrName string, architecture Architecture) (*Image, *Response, error) {
148151
if id, err := strconv.ParseInt(idOrName, 10, 64); err == nil {
149-
return c.GetByID(ctx, id)
152+
img, res, err := c.GetByID(ctx, id)
153+
if img != nil || err != nil {
154+
return img, res, err
155+
}
150156
}
151157
return c.GetByNameAndArchitecture(ctx, idOrName, architecture)
152158
}

hcloud/image_test.go

+41
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,47 @@ func TestImageClient(t *testing.T) {
134134
})
135135
})
136136

137+
t.Run("GetByNumericName", func(t *testing.T) {
138+
env := newTestEnv()
139+
defer env.Teardown()
140+
141+
env.Mux.HandleFunc("/images/123", func(w http.ResponseWriter, r *http.Request) {
142+
w.Header().Set("Content-Type", "application/json")
143+
w.WriteHeader(http.StatusNotFound)
144+
json.NewEncoder(w).Encode(schema.ErrorResponse{
145+
Error: schema.Error{
146+
Code: string(ErrorCodeNotFound),
147+
},
148+
})
149+
})
150+
151+
env.Mux.HandleFunc("/images", func(w http.ResponseWriter, r *http.Request) {
152+
if r.URL.RawQuery != "name=123" {
153+
t.Fatal("missing name query")
154+
}
155+
json.NewEncoder(w).Encode(schema.ImageListResponse{
156+
Images: []schema.Image{
157+
{
158+
ID: 1,
159+
},
160+
},
161+
})
162+
})
163+
164+
ctx := context.Background()
165+
166+
image, _, err := env.Client.Image.Get(ctx, "123")
167+
if err != nil {
168+
t.Fatal(err)
169+
}
170+
if image == nil {
171+
t.Fatal("no image")
172+
}
173+
if image.ID != 1 {
174+
t.Errorf("unexpected image ID: %v", image.ID)
175+
}
176+
})
177+
137178
t.Run("GetByName (not found)", func(t *testing.T) {
138179
env := newTestEnv()
139180
defer env.Teardown()

hcloud/iso.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,10 @@ func (c *ISOClient) GetByName(ctx context.Context, name string) (*ISO, *Response
7171
// Get retrieves an ISO by its ID if the input can be parsed as an integer, otherwise it retrieves an ISO by its name.
7272
func (c *ISOClient) Get(ctx context.Context, idOrName string) (*ISO, *Response, error) {
7373
if id, err := strconv.ParseInt(idOrName, 10, 64); err == nil {
74-
return c.GetByID(ctx, id)
74+
iso, res, err := c.GetByID(ctx, id)
75+
if iso != nil || err != nil {
76+
return iso, res, err
77+
}
7578
}
7679
return c.GetByName(ctx, idOrName)
7780
}

hcloud/iso_test.go

+41
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,47 @@ func TestISOClient(t *testing.T) {
134134
})
135135
})
136136

137+
t.Run("GetByNumericName", func(t *testing.T) {
138+
env := newTestEnv()
139+
defer env.Teardown()
140+
141+
env.Mux.HandleFunc("/isos/123", func(w http.ResponseWriter, r *http.Request) {
142+
w.Header().Set("Content-Type", "application/json")
143+
w.WriteHeader(http.StatusNotFound)
144+
json.NewEncoder(w).Encode(schema.ErrorResponse{
145+
Error: schema.Error{
146+
Code: string(ErrorCodeNotFound),
147+
},
148+
})
149+
})
150+
151+
env.Mux.HandleFunc("/isos", func(w http.ResponseWriter, r *http.Request) {
152+
if r.URL.RawQuery != "name=123" {
153+
t.Fatal("missing name query")
154+
}
155+
json.NewEncoder(w).Encode(schema.ISOListResponse{
156+
ISOs: []schema.ISO{
157+
{
158+
ID: 1,
159+
},
160+
},
161+
})
162+
})
163+
164+
ctx := context.Background()
165+
166+
iso, _, err := env.Client.ISO.Get(ctx, "123")
167+
if err != nil {
168+
t.Fatal(err)
169+
}
170+
if iso == nil {
171+
t.Fatal("no iso")
172+
}
173+
if iso.ID != 1 {
174+
t.Errorf("unexpected iso ID: %v", iso.ID)
175+
}
176+
})
177+
137178
t.Run("GetByName (not found)", func(t *testing.T) {
138179
env := newTestEnv()
139180
defer env.Teardown()

hcloud/load_balancer.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,10 @@ func (c *LoadBalancerClient) GetByName(ctx context.Context, name string) (*LoadB
275275
// retrieves a Load Balancer by its name. If the Load Balancer does not exist, nil is returned.
276276
func (c *LoadBalancerClient) Get(ctx context.Context, idOrName string) (*LoadBalancer, *Response, error) {
277277
if id, err := strconv.ParseInt(idOrName, 10, 64); err == nil {
278-
return c.GetByID(ctx, id)
278+
lb, res, err := c.GetByID(ctx, id)
279+
if lb != nil || err != nil {
280+
return lb, res, err
281+
}
279282
}
280283
return c.GetByName(ctx, idOrName)
281284
}

hcloud/load_balancer_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,48 @@ func TestLoadBalancerClientGetByName(t *testing.T) {
125125
})
126126
}
127127

128+
func TestLoadBalancerClientGetByNumericName(t *testing.T) {
129+
env := newTestEnv()
130+
defer env.Teardown()
131+
132+
env.Mux.HandleFunc("/load_balancers/123", func(w http.ResponseWriter, r *http.Request) {
133+
w.Header().Set("Content-Type", "application/json")
134+
w.WriteHeader(http.StatusNotFound)
135+
json.NewEncoder(w).Encode(schema.ErrorResponse{
136+
Error: schema.Error{
137+
Code: string(ErrorCodeNotFound),
138+
},
139+
})
140+
})
141+
142+
env.Mux.HandleFunc("/load_balancers", func(w http.ResponseWriter, r *http.Request) {
143+
if r.URL.RawQuery != "name=123" {
144+
t.Fatal("missing name query")
145+
}
146+
json.NewEncoder(w).Encode(schema.LoadBalancerListResponse{
147+
LoadBalancers: []schema.LoadBalancer{
148+
{
149+
ID: 1,
150+
Name: "123",
151+
},
152+
},
153+
})
154+
})
155+
156+
ctx := context.Background()
157+
158+
loadBalancer, _, err := env.Client.LoadBalancer.Get(ctx, "123")
159+
if err != nil {
160+
t.Fatal(err)
161+
}
162+
if loadBalancer == nil {
163+
t.Fatal("no load balancer")
164+
}
165+
if loadBalancer.ID != 1 {
166+
t.Errorf("unexpected load balancer ID: %v", loadBalancer.ID)
167+
}
168+
}
169+
128170
func TestLoadBalancerClientGetByNameNotFound(t *testing.T) {
129171
env := newTestEnv()
130172
defer env.Teardown()

hcloud/network.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,10 @@ func (c *NetworkClient) GetByName(ctx context.Context, name string) (*Network, *
119119
// retrieves a network by its name. If the network does not exist, nil is returned.
120120
func (c *NetworkClient) Get(ctx context.Context, idOrName string) (*Network, *Response, error) {
121121
if id, err := strconv.ParseInt(idOrName, 10, 64); err == nil {
122-
return c.GetByID(ctx, id)
122+
n, res, err := c.GetByID(ctx, id)
123+
if n != nil || err != nil {
124+
return n, res, err
125+
}
123126
}
124127
return c.GetByName(ctx, idOrName)
125128
}

0 commit comments

Comments
 (0)