Skip to content

Commit 01857ca

Browse files
authored
fix(builder): add network interface validation (#107)
Introduce a validator function for network interfaces for stricter network checks. It enforces interface type, private network UUID and UUID format. Also exported MaxTemplateNameLength/MaxTemplatePrefixLength constants. This is useful for test packages. Update tests to cover network interface validation cases. Add google/uuid v1.6.0 as direct dependency for UUID validation. Signed-off-by: Ville Vesilehto <ville.vesilehto@upcloud.com>
1 parent 3c6654f commit 01857ca

File tree

4 files changed

+248
-12
lines changed

4 files changed

+248
-12
lines changed

builder/upcloud/config.go

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ package upcloud
55
import (
66
"errors"
77
"fmt"
8+
"net"
89
"time"
910

11+
"github.com/google/uuid"
1012
"github.com/hashicorp/packer-plugin-sdk/common"
1113
"github.com/hashicorp/packer-plugin-sdk/communicator"
1214
"github.com/hashicorp/packer-plugin-sdk/packer"
@@ -29,8 +31,8 @@ const (
2931
InterfaceTypePublic InterfaceType = upcloud.IPAddressAccessPublic
3032
InterfaceTypeUtility InterfaceType = upcloud.IPAddressAccessUtility
3133
InterfaceTypePrivate InterfaceType = upcloud.IPAddressAccessPrivate
32-
maxTemplateNameLength = 40
33-
maxTemplatePrefixLength = 40
34+
MaxTemplateNameLength = 40
35+
MaxTemplatePrefixLength = 40
3436
)
3537

3638
// for config type conversion.
@@ -210,20 +212,25 @@ func (c *Config) validate() *packer.MultiError {
210212
errs = packer.MultiErrorAppend(errs, templateErrs.Errors...)
211213
}
212214

215+
// Validate network interfaces
216+
if networkErrs := c.validateNetworkInterfaces(); networkErrs != nil {
217+
errs = packer.MultiErrorAppend(errs, networkErrs.Errors...)
218+
}
219+
213220
return errs
214221
}
215222

216223
// validateTemplate checks template configuration.
217224
func (c *Config) validateTemplate() *packer.MultiError {
218225
var errs *packer.MultiError
219226

220-
if len(c.TemplatePrefix) > maxTemplatePrefixLength {
227+
if len(c.TemplatePrefix) > MaxTemplatePrefixLength {
221228
errs = packer.MultiErrorAppend(
222229
errs, errors.New("'template_prefix' must be 0-40 characters"),
223230
)
224231
}
225232

226-
if len(c.TemplateName) > maxTemplateNameLength {
233+
if len(c.TemplateName) > MaxTemplateNameLength {
227234
errs = packer.MultiErrorAppend(
228235
errs, errors.New("'template_name' is limited to 40 characters"),
229236
)
@@ -250,3 +257,58 @@ func (c *Config) SetEnv() error {
250257
c.Token = creds.Token
251258
return nil
252259
}
260+
261+
// validateNetworkInterfaces checks network interface configuration.
262+
func (c *Config) validateNetworkInterfaces() *packer.MultiError {
263+
var errs *packer.MultiError
264+
265+
for i, iface := range c.NetworkInterfaces {
266+
// Validate interface type
267+
switch iface.Type {
268+
case InterfaceTypePublic, InterfaceTypePrivate, InterfaceTypeUtility:
269+
// valid
270+
default:
271+
errs = packer.MultiErrorAppend(errs, fmt.Errorf("network interface %d has invalid type: %s", i, iface.Type))
272+
}
273+
274+
// Validate network UUID for private networks
275+
if iface.Type == InterfaceTypePrivate {
276+
if iface.Network == "" {
277+
errs = packer.MultiErrorAppend(errs, fmt.Errorf("network interface %d: private network requires network UUID", i))
278+
} else if _, err := uuid.Parse(iface.Network); err != nil {
279+
errs = packer.MultiErrorAppend(errs, fmt.Errorf("network interface %d: invalid network UUID '%s'", i, iface.Network))
280+
}
281+
}
282+
283+
// Validate IP addresses
284+
for j, ip := range iface.IPAddresses {
285+
if ip.Family != upcloud.IPAddressFamilyIPv4 && ip.Family != upcloud.IPAddressFamilyIPv6 {
286+
errs = packer.MultiErrorAppend(errs, fmt.Errorf("network interface %d, IP %d: invalid IP family '%s'", i, j, ip.Family))
287+
}
288+
289+
if ip.Address == "" {
290+
continue
291+
}
292+
293+
parsedIP := net.ParseIP(ip.Address)
294+
if parsedIP == nil {
295+
errs = packer.MultiErrorAppend(errs, fmt.Errorf("network interface %d, IP %d: invalid IP address '%s'", i, j, ip.Address))
296+
continue
297+
}
298+
299+
isIPv4 := parsedIP.To4() != nil
300+
switch ip.Family {
301+
case upcloud.IPAddressFamilyIPv4:
302+
if !isIPv4 {
303+
errs = packer.MultiErrorAppend(errs, fmt.Errorf("network interface %d, IP %d: IP family is IPv4 but address is IPv6", i, j))
304+
}
305+
case upcloud.IPAddressFamilyIPv6:
306+
if isIPv4 {
307+
errs = packer.MultiErrorAppend(errs, fmt.Errorf("network interface %d, IP %d: IP family is IPv6 but address is IPv4", i, j))
308+
}
309+
}
310+
}
311+
}
312+
313+
return errs
314+
}

builder/upcloud/config_test.go

Lines changed: 179 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ func TestConfig_Prepare_BothAuthMethods(t *testing.T) {
6868
warns, err := c.Prepare(raws...)
6969
assert.NoError(t, err)
7070
assert.Equal(t, "test-api-token", c.Token)
71-
assert.Equal(t, "", c.Username)
72-
assert.Equal(t, "", c.Password)
71+
assert.Empty(t, c.Username)
72+
assert.Empty(t, c.Password)
7373
assert.Empty(t, warns)
7474
}
7575

@@ -306,7 +306,7 @@ func TestConfig_setEnv_DoesNotOverrideExisting_basic(t *testing.T) {
306306
// Should not override existing values
307307
assert.Equal(t, "existing-user", c.Username)
308308
assert.Equal(t, "existing-pass", c.Password)
309-
assert.Equal(t, "", c.Token)
309+
assert.Empty(t, c.Token)
310310
}
311311

312312
func TestConfig_setEnv_DoesNotOverrideExisting_token(t *testing.T) {
@@ -323,8 +323,8 @@ func TestConfig_setEnv_DoesNotOverrideExisting_token(t *testing.T) {
323323
assert.NoError(t, err)
324324

325325
// Should not override existing values
326-
assert.Equal(t, "", c.Username)
327-
assert.Equal(t, "", c.Password)
326+
assert.Empty(t, c.Username)
327+
assert.Empty(t, c.Password)
328328
assert.Equal(t, "existing-token", c.Token)
329329
}
330330

@@ -413,3 +413,177 @@ func TestConfig_Prepare_CustomValues(t *testing.T) {
413413
assert.Equal(t, 30*time.Second, c.BootWait)
414414
assert.Equal(t, []string{"nl-ams1", "us-nyc1"}, c.CloneZones)
415415
}
416+
417+
func TestConfig_validateNetworkInterfaces(t *testing.T) {
418+
t.Parallel()
419+
tests := []struct {
420+
name string
421+
interfaces []upcloud.NetworkInterface
422+
expectError bool
423+
errorSubstring string
424+
}{
425+
{
426+
name: "valid public interface",
427+
interfaces: []upcloud.NetworkInterface{
428+
{
429+
Type: upcloud.InterfaceTypePublic,
430+
IPAddresses: []upcloud.IPAddress{
431+
{Family: "IPv4", Default: true},
432+
},
433+
},
434+
},
435+
expectError: false,
436+
},
437+
{
438+
name: "valid private interface with UUID",
439+
interfaces: []upcloud.NetworkInterface{
440+
{
441+
Type: upcloud.InterfaceTypePrivate,
442+
Network: "01234567-89ab-cdef-0123-456789abcdef",
443+
IPAddresses: []upcloud.IPAddress{
444+
{Family: "IPv4", Address: "192.168.1.100"},
445+
},
446+
},
447+
},
448+
expectError: false,
449+
},
450+
{
451+
name: "invalid interface type",
452+
interfaces: []upcloud.NetworkInterface{
453+
{Type: upcloud.InterfaceType("invalid")},
454+
},
455+
expectError: true,
456+
errorSubstring: "has invalid type: invalid",
457+
},
458+
{
459+
name: "private interface without UUID",
460+
interfaces: []upcloud.NetworkInterface{
461+
{Type: upcloud.InterfaceTypePrivate},
462+
},
463+
expectError: true,
464+
errorSubstring: "private network requires network UUID",
465+
},
466+
{
467+
name: "private interface with invalid UUID",
468+
interfaces: []upcloud.NetworkInterface{
469+
{
470+
Type: upcloud.InterfaceTypePrivate,
471+
Network: "not-a-uuid",
472+
},
473+
},
474+
expectError: true,
475+
errorSubstring: "invalid network UUID",
476+
},
477+
{
478+
name: "invalid IP family",
479+
interfaces: []upcloud.NetworkInterface{
480+
{
481+
Type: upcloud.InterfaceTypePublic,
482+
IPAddresses: []upcloud.IPAddress{
483+
{Family: "IPv5"},
484+
},
485+
},
486+
},
487+
expectError: true,
488+
errorSubstring: "invalid IP family 'IPv5'",
489+
},
490+
{
491+
name: "invalid IP address",
492+
interfaces: []upcloud.NetworkInterface{
493+
{
494+
Type: upcloud.InterfaceTypePublic,
495+
IPAddresses: []upcloud.IPAddress{
496+
{Family: "IPv4", Address: "invalid-ip"},
497+
},
498+
},
499+
},
500+
expectError: true,
501+
errorSubstring: "invalid IP address 'invalid-ip'",
502+
},
503+
{
504+
name: "IP family mismatch - IPv4 family with IPv6 address",
505+
interfaces: []upcloud.NetworkInterface{
506+
{
507+
Type: upcloud.InterfaceTypePublic,
508+
IPAddresses: []upcloud.IPAddress{
509+
{Family: "IPv4", Address: "2001:db8::1"},
510+
},
511+
},
512+
},
513+
expectError: true,
514+
errorSubstring: "IP family is IPv4 but address is IPv6",
515+
},
516+
{
517+
name: "IP family mismatch - IPv6 family with IPv4 address",
518+
interfaces: []upcloud.NetworkInterface{
519+
{
520+
Type: upcloud.InterfaceTypePublic,
521+
IPAddresses: []upcloud.IPAddress{
522+
{Family: "IPv6", Address: "192.168.1.1"},
523+
},
524+
},
525+
},
526+
expectError: true,
527+
errorSubstring: "IP family is IPv6 but address is IPv4",
528+
},
529+
{
530+
name: "multiple interfaces with mixed errors",
531+
interfaces: []upcloud.NetworkInterface{
532+
{
533+
Type: upcloud.InterfaceTypePublic,
534+
IPAddresses: []upcloud.IPAddress{
535+
{Family: "IPv4", Default: true},
536+
},
537+
},
538+
{
539+
Type: upcloud.InterfaceType("invalid"),
540+
},
541+
{
542+
Type: upcloud.InterfaceTypePrivate,
543+
Network: "bad-uuid",
544+
},
545+
},
546+
expectError: true,
547+
errorSubstring: "has invalid type",
548+
},
549+
{
550+
name: "empty interfaces slice",
551+
interfaces: []upcloud.NetworkInterface{},
552+
expectError: false,
553+
},
554+
{
555+
name: "interface with empty IP addresses",
556+
interfaces: []upcloud.NetworkInterface{
557+
{
558+
Type: upcloud.InterfaceTypeUtility,
559+
IPAddresses: []upcloud.IPAddress{},
560+
},
561+
},
562+
expectError: false,
563+
},
564+
}
565+
566+
for _, tt := range tests {
567+
t.Run(tt.name, func(t *testing.T) {
568+
t.Parallel()
569+
c := &upcloud.Config{
570+
Username: "testuser",
571+
Password: "testpass",
572+
Zone: "fi-hel1",
573+
StorageUUID: "01000000-0000-4000-8000-000030060200",
574+
NetworkInterfaces: tt.interfaces,
575+
}
576+
577+
_, err := c.Prepare(map[string]interface{}{})
578+
579+
if tt.expectError {
580+
assert.Error(t, err, "expected validation errors")
581+
if tt.errorSubstring != "" {
582+
assert.Contains(t, err.Error(), tt.errorSubstring)
583+
}
584+
} else {
585+
assert.NoError(t, err, "unexpected validation errors")
586+
}
587+
})
588+
}
589+
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.25.0
55
require (
66
github.com/UpCloudLtd/upcloud-go-api/credentials v0.1.1
77
github.com/UpCloudLtd/upcloud-go-api/v8 v8.25.0
8+
github.com/google/uuid v1.6.0
89
github.com/hashicorp/hcl/v2 v2.24.0
910
github.com/hashicorp/packer-plugin-sdk v0.6.2
1011
github.com/stretchr/testify v1.11.1
@@ -39,7 +40,6 @@ require (
3940
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
4041
github.com/golang/protobuf v1.5.3 // indirect
4142
github.com/google/s2a-go v0.1.7 // indirect
42-
github.com/google/uuid v1.4.0 // indirect
4343
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
4444
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
4545
github.com/hashicorp/consul/api v1.25.1 // indirect

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,8 @@ github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8
135135
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
136136
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
137137
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
138-
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
139-
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
138+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
139+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
140140
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
141141
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
142142
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=

0 commit comments

Comments
 (0)