Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 66 additions & 4 deletions builder/upcloud/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand Down Expand Up @@ -210,20 +212,25 @@ 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
}

// validateTemplate checks template configuration.
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"),
)
Expand All @@ -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
}
184 changes: 179 additions & 5 deletions builder/upcloud/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}

Expand Down Expand Up @@ -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")
}
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
Loading