diff --git a/builder/upcloud/config.go b/builder/upcloud/config.go index 07026e7..7dd5b34 100644 --- a/builder/upcloud/config.go +++ b/builder/upcloud/config.go @@ -5,8 +5,10 @@ package upcloud import ( "errors" "fmt" + "net" "time" + "github.com/google/uuid" "github.com/hashicorp/packer-plugin-sdk/common" "github.com/hashicorp/packer-plugin-sdk/communicator" "github.com/hashicorp/packer-plugin-sdk/packer" @@ -29,8 +31,8 @@ const ( InterfaceTypePublic InterfaceType = upcloud.IPAddressAccessPublic InterfaceTypeUtility InterfaceType = upcloud.IPAddressAccessUtility InterfaceTypePrivate InterfaceType = upcloud.IPAddressAccessPrivate - maxTemplateNameLength = 40 - maxTemplatePrefixLength = 40 + MaxTemplateNameLength = 40 + MaxTemplatePrefixLength = 40 ) // for config type conversion. @@ -210,6 +212,11 @@ func (c *Config) validate() *packer.MultiError { errs = packer.MultiErrorAppend(errs, templateErrs.Errors...) } + // Validate network interfaces + if networkErrs := c.validateNetworkInterfaces(); networkErrs != nil { + errs = packer.MultiErrorAppend(errs, networkErrs.Errors...) + } + return errs } @@ -217,13 +224,13 @@ func (c *Config) validate() *packer.MultiError { func (c *Config) validateTemplate() *packer.MultiError { var errs *packer.MultiError - if len(c.TemplatePrefix) > maxTemplatePrefixLength { + if len(c.TemplatePrefix) > MaxTemplatePrefixLength { errs = packer.MultiErrorAppend( errs, errors.New("'template_prefix' must be 0-40 characters"), ) } - if len(c.TemplateName) > maxTemplateNameLength { + if len(c.TemplateName) > MaxTemplateNameLength { errs = packer.MultiErrorAppend( errs, errors.New("'template_name' is limited to 40 characters"), ) @@ -250,3 +257,58 @@ func (c *Config) SetEnv() error { c.Token = creds.Token return nil } + +// validateNetworkInterfaces checks network interface configuration. +func (c *Config) validateNetworkInterfaces() *packer.MultiError { + var errs *packer.MultiError + + for i, iface := range c.NetworkInterfaces { + // Validate interface type + switch iface.Type { + case InterfaceTypePublic, InterfaceTypePrivate, InterfaceTypeUtility: + // valid + default: + errs = packer.MultiErrorAppend(errs, fmt.Errorf("network interface %d has invalid type: %s", i, iface.Type)) + } + + // Validate network UUID for private networks + if iface.Type == InterfaceTypePrivate { + if iface.Network == "" { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("network interface %d: private network requires network UUID", i)) + } else if _, err := uuid.Parse(iface.Network); err != nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("network interface %d: invalid network UUID '%s'", i, iface.Network)) + } + } + + // Validate IP addresses + for j, ip := range iface.IPAddresses { + if ip.Family != upcloud.IPAddressFamilyIPv4 && ip.Family != upcloud.IPAddressFamilyIPv6 { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("network interface %d, IP %d: invalid IP family '%s'", i, j, ip.Family)) + } + + if ip.Address == "" { + continue + } + + parsedIP := net.ParseIP(ip.Address) + if parsedIP == nil { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("network interface %d, IP %d: invalid IP address '%s'", i, j, ip.Address)) + continue + } + + isIPv4 := parsedIP.To4() != nil + switch ip.Family { + case upcloud.IPAddressFamilyIPv4: + if !isIPv4 { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("network interface %d, IP %d: IP family is IPv4 but address is IPv6", i, j)) + } + case upcloud.IPAddressFamilyIPv6: + if isIPv4 { + errs = packer.MultiErrorAppend(errs, fmt.Errorf("network interface %d, IP %d: IP family is IPv6 but address is IPv4", i, j)) + } + } + } + } + + return errs +} diff --git a/builder/upcloud/config_test.go b/builder/upcloud/config_test.go index e9fe2cb..f42323b 100644 --- a/builder/upcloud/config_test.go +++ b/builder/upcloud/config_test.go @@ -68,8 +68,8 @@ func TestConfig_Prepare_BothAuthMethods(t *testing.T) { warns, err := c.Prepare(raws...) assert.NoError(t, err) assert.Equal(t, "test-api-token", c.Token) - assert.Equal(t, "", c.Username) - assert.Equal(t, "", c.Password) + assert.Empty(t, c.Username) + assert.Empty(t, c.Password) assert.Empty(t, warns) } @@ -306,7 +306,7 @@ func TestConfig_setEnv_DoesNotOverrideExisting_basic(t *testing.T) { // Should not override existing values assert.Equal(t, "existing-user", c.Username) assert.Equal(t, "existing-pass", c.Password) - assert.Equal(t, "", c.Token) + assert.Empty(t, c.Token) } func TestConfig_setEnv_DoesNotOverrideExisting_token(t *testing.T) { @@ -323,8 +323,8 @@ func TestConfig_setEnv_DoesNotOverrideExisting_token(t *testing.T) { assert.NoError(t, err) // Should not override existing values - assert.Equal(t, "", c.Username) - assert.Equal(t, "", c.Password) + assert.Empty(t, c.Username) + assert.Empty(t, c.Password) assert.Equal(t, "existing-token", c.Token) } @@ -413,3 +413,177 @@ func TestConfig_Prepare_CustomValues(t *testing.T) { assert.Equal(t, 30*time.Second, c.BootWait) assert.Equal(t, []string{"nl-ams1", "us-nyc1"}, c.CloneZones) } + +func TestConfig_validateNetworkInterfaces(t *testing.T) { + t.Parallel() + tests := []struct { + name string + interfaces []upcloud.NetworkInterface + expectError bool + errorSubstring string + }{ + { + name: "valid public interface", + interfaces: []upcloud.NetworkInterface{ + { + Type: upcloud.InterfaceTypePublic, + IPAddresses: []upcloud.IPAddress{ + {Family: "IPv4", Default: true}, + }, + }, + }, + expectError: false, + }, + { + name: "valid private interface with UUID", + interfaces: []upcloud.NetworkInterface{ + { + Type: upcloud.InterfaceTypePrivate, + Network: "01234567-89ab-cdef-0123-456789abcdef", + IPAddresses: []upcloud.IPAddress{ + {Family: "IPv4", Address: "192.168.1.100"}, + }, + }, + }, + expectError: false, + }, + { + name: "invalid interface type", + interfaces: []upcloud.NetworkInterface{ + {Type: upcloud.InterfaceType("invalid")}, + }, + expectError: true, + errorSubstring: "has invalid type: invalid", + }, + { + name: "private interface without UUID", + interfaces: []upcloud.NetworkInterface{ + {Type: upcloud.InterfaceTypePrivate}, + }, + expectError: true, + errorSubstring: "private network requires network UUID", + }, + { + name: "private interface with invalid UUID", + interfaces: []upcloud.NetworkInterface{ + { + Type: upcloud.InterfaceTypePrivate, + Network: "not-a-uuid", + }, + }, + expectError: true, + errorSubstring: "invalid network UUID", + }, + { + name: "invalid IP family", + interfaces: []upcloud.NetworkInterface{ + { + Type: upcloud.InterfaceTypePublic, + IPAddresses: []upcloud.IPAddress{ + {Family: "IPv5"}, + }, + }, + }, + expectError: true, + errorSubstring: "invalid IP family 'IPv5'", + }, + { + name: "invalid IP address", + interfaces: []upcloud.NetworkInterface{ + { + Type: upcloud.InterfaceTypePublic, + IPAddresses: []upcloud.IPAddress{ + {Family: "IPv4", Address: "invalid-ip"}, + }, + }, + }, + expectError: true, + errorSubstring: "invalid IP address 'invalid-ip'", + }, + { + name: "IP family mismatch - IPv4 family with IPv6 address", + interfaces: []upcloud.NetworkInterface{ + { + Type: upcloud.InterfaceTypePublic, + IPAddresses: []upcloud.IPAddress{ + {Family: "IPv4", Address: "2001:db8::1"}, + }, + }, + }, + expectError: true, + errorSubstring: "IP family is IPv4 but address is IPv6", + }, + { + name: "IP family mismatch - IPv6 family with IPv4 address", + interfaces: []upcloud.NetworkInterface{ + { + Type: upcloud.InterfaceTypePublic, + IPAddresses: []upcloud.IPAddress{ + {Family: "IPv6", Address: "192.168.1.1"}, + }, + }, + }, + expectError: true, + errorSubstring: "IP family is IPv6 but address is IPv4", + }, + { + name: "multiple interfaces with mixed errors", + interfaces: []upcloud.NetworkInterface{ + { + Type: upcloud.InterfaceTypePublic, + IPAddresses: []upcloud.IPAddress{ + {Family: "IPv4", Default: true}, + }, + }, + { + Type: upcloud.InterfaceType("invalid"), + }, + { + Type: upcloud.InterfaceTypePrivate, + Network: "bad-uuid", + }, + }, + expectError: true, + errorSubstring: "has invalid type", + }, + { + name: "empty interfaces slice", + interfaces: []upcloud.NetworkInterface{}, + expectError: false, + }, + { + name: "interface with empty IP addresses", + interfaces: []upcloud.NetworkInterface{ + { + Type: upcloud.InterfaceTypeUtility, + IPAddresses: []upcloud.IPAddress{}, + }, + }, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + c := &upcloud.Config{ + Username: "testuser", + Password: "testpass", + Zone: "fi-hel1", + StorageUUID: "01000000-0000-4000-8000-000030060200", + NetworkInterfaces: tt.interfaces, + } + + _, err := c.Prepare(map[string]interface{}{}) + + if tt.expectError { + assert.Error(t, err, "expected validation errors") + if tt.errorSubstring != "" { + assert.Contains(t, err.Error(), tt.errorSubstring) + } + } else { + assert.NoError(t, err, "unexpected validation errors") + } + }) + } +} diff --git a/go.mod b/go.mod index 430a53d..1eadf32 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.0 require ( github.com/UpCloudLtd/upcloud-go-api/credentials v0.1.1 github.com/UpCloudLtd/upcloud-go-api/v8 v8.25.0 + github.com/google/uuid v1.6.0 github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/packer-plugin-sdk v0.6.2 github.com/stretchr/testify v1.11.1 @@ -39,7 +40,6 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/s2a-go v0.1.7 // indirect - github.com/google/uuid v1.4.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/hashicorp/consul/api v1.25.1 // indirect diff --git a/go.sum b/go.sum index 7f06b5d..1b4500a 100644 --- a/go.sum +++ b/go.sum @@ -135,8 +135,8 @@ github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=