diff --git a/.web-docs/components/builder/linode/README.md b/.web-docs/components/builder/linode/README.md index 0b562880..ffcbbe41 100644 --- a/.web-docs/components/builder/linode/README.md +++ b/.web-docs/components/builder/linode/README.md @@ -88,7 +88,7 @@ can also be supplied to override the typical auto-generated key: for more information on the Images available for use. Examples are `linode/debian12`, `linode/debian13`, `linode/ubuntu24.04`, `linode/arch`, and `private/12345`. -- `swap_size` (int) - The disk size (MiB) allocated for swap space. +- `swap_size` (\*int) - The disk size (MiB) allocated for swap space. - `private_ip` (bool) - If true, the created Linode will have private networking enabled and assigned a private IPv4 address. @@ -275,6 +275,8 @@ This section outlines the fields configurable for a newer Linode interface objec - `ipv4` (\*VPCInterfaceIPv4) - Interfaces can be configured with IPv4 addresses or ranges. +- `ipv6` (\*VPCInterfaceIPv6) - IPv6 configuration for this VPC interface. + @@ -330,6 +332,43 @@ This section outlines the fields configurable for a newer Linode interface objec +##### VPC Linode Interface IPv6 configuration object (VPCInterfaceIPv6) + +###### Optional + + + +- `slaac` ([]VPCInterfaceIPv6SLAAC) - IPv6 SLAAC settings for this VPC interface. + +- `ranges` ([]VPCInterfaceIPv6Range) - IPv6 ranges for this VPC interface. + +- `is_public` (\*bool) - Whether the IPv6 addresses are publicly routable. + + + + +##### VPC Linode Interface IPv6 SLAAC configuration object (VPCInterfaceIPv6SLAAC) + +###### Required + + + +- `range` (string) - The IPv6 SLAAC range for this VPC interface. + + + + +##### VPC Linode Interface IPv6 Range configuration object (VPCInterfaceIPv6Range) + +###### Required + + + +- `range` (string) - The IPv6 range for this VPC interface. + + + + ##### VLAN Linode Interface configuration object (VLANInterface) ###### Required diff --git a/builder/linode/builder_test.go b/builder/linode/builder_test.go index 9b310199..b88dfdd3 100644 --- a/builder/linode/builder_test.go +++ b/builder/linode/builder_test.go @@ -123,6 +123,44 @@ func TestBuilderPrepare_Size(t *testing.T) { } } +func TestBuilderPrepare_SwapSize(t *testing.T) { + t.Run("omitted remains nil", func(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "swap_size") + + _, warnings, err := b.Prepare(config) + if len(warnings) > 0 { + t.Fatalf("bad: %#v", warnings) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.SwapSize != nil { + t.Fatalf("swap_size = %v, want nil", b.config.SwapSize) + } + }) + + t.Run("explicit zero remains non-nil", func(t *testing.T) { + var b Builder + config := testConfig() + config["swap_size"] = 0 + + _, warnings, err := b.Prepare(config) + if len(warnings) > 0 { + t.Fatalf("bad: %#v", warnings) + } + if err != nil { + t.Fatalf("should not have error: %s", err) + } + + if b.config.SwapSize == nil || *b.config.SwapSize != 0 { + t.Fatalf("swap_size = %v, want pointer to 0", b.config.SwapSize) + } + }) +} + func TestBuilderPrepare_Image(t *testing.T) { var b Builder config := testConfig() @@ -531,6 +569,71 @@ func TestBuilderPrepare_LinodeNetworkInterfaces(t *testing.T) { t.Fatalf("unexpected error: %v", err) } + config["linode_interface"] = []map[string]any{ + { + "firewall_id": 123, + "default_route": map[string]any{ + "ipv4": true, + "ipv6": true, + }, + "public": map[string]any{ + "ipv4": map[string]any{ + "address": []map[string]any{ + { + "address": "auto", + "primary": true, + }, + }, + }, + "ipv6": map[string]any{ + "ranges": []map[string]any{ + { + "range": "/64", + }, + }, + }, + }, + }, + { + "firewall_id": 123, + "default_route": map[string]any{ + "ipv4": false, + "ipv6": false, + }, + "vpc": map[string]any{ + "subnet_id": 12345, + "ipv4": map[string]any{ + "addresses": []map[string]any{ + {"address": "auto", "primary": false, "nat_1_1_address": "auto"}, + }, + }, + "ipv6": map[string]any{ + "slaac": []map[string]any{ + { + "range": "2600:3c03:e000:123::/64", + }, + }, + "ranges": []map[string]any{ + { + "range": "2600:3c03:e000:123:1::/64", + }, + }, + "is_public": true, + }, + }, + }, + { + "default_route": map[string]any{ + "ipv4": false, + "ipv6": false, + }, + "vlan": map[string]any{ + "vlan_label": "vlan-1", + "ipam_address": "10.0.0.1/24", + }, + }, + } + expectedLinodeInterfaces := []LinodeInterface{ { FirewallID: linodego.Pointer(123), @@ -573,6 +676,19 @@ func TestBuilderPrepare_LinodeNetworkInterfaces(t *testing.T) { }, }, }, + IPv6: &VPCInterfaceIPv6{ + SLAAC: []VPCInterfaceIPv6SLAAC{ + { + Range: "2600:3c03:e000:123::/64", + }, + }, + Ranges: []VPCInterfaceIPv6Range{ + { + Range: "2600:3c03:e000:123:1::/64", + }, + }, + IsPublic: linodego.Pointer(true), + }, }, }, { @@ -587,8 +703,6 @@ func TestBuilderPrepare_LinodeNetworkInterfaces(t *testing.T) { }, } - // Test set - config["linode_interface"] = expectedLinodeInterfaces b = Builder{} _, warnings, err = b.Prepare(config) if len(warnings) > 0 { @@ -989,6 +1103,27 @@ func TestBuilderPrepare_CustomDisksValidation(t *testing.T) { } }) + t.Run("IncompatibleSwapSizeZero", func(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "image") + config["swap_size"] = 0 + config["disk"] = []map[string]any{ + {"label": "boot", "size": 25000, "image": "linode/arch"}, + } + config["config"] = []map[string]any{ + {"label": "my-config"}, + } + + _, _, err := b.Prepare(config) + if err == nil { + t.Fatal("expected error with swap_size=0 and custom disks") + } + if !strings.Contains(err.Error(), "swap_size cannot be specified when using custom disks") { + t.Fatalf("expected specific error message, got: %s", err) + } + }) + t.Run("IncompatibleStackScriptID", func(t *testing.T) { var b Builder config := testConfig() diff --git a/builder/linode/config.go b/builder/linode/config.go index 7e209ea5..fc0d90ec 100644 --- a/builder/linode/config.go +++ b/builder/linode/config.go @@ -288,7 +288,7 @@ type Config struct { Image string `mapstructure:"image" required:"false"` // The disk size (MiB) allocated for swap space. - SwapSize int `mapstructure:"swap_size" required:"false"` + SwapSize *int `mapstructure:"swap_size" required:"false"` // If true, the created Linode will have private networking enabled and assigned // a private IPv4 address. @@ -720,7 +720,7 @@ func (c *Config) Prepare(raws ...any) ([]string, error) { errs, errors.New("authorized_users cannot be specified when using custom disks (specify in disk blocks instead)")) } - if c.SwapSize > 0 { + if c.SwapSize != nil { errs = packersdk.MultiErrorAppend( errs, errors.New("swap_size cannot be specified when using custom disks (create a swap disk instead)")) } diff --git a/builder/linode/linode_interfaces.go b/builder/linode/linode_interfaces.go index 9281aee1..ef4945b7 100644 --- a/builder/linode/linode_interfaces.go +++ b/builder/linode/linode_interfaces.go @@ -1,5 +1,5 @@ //go:generate packer-sdc struct-markdown -//go:generate packer-sdc mapstructure-to-hcl2 -type LinodeInterface,InterfaceDefaultRoute,PublicInterface,PublicInterfaceIPv4,PublicInterfaceIPv6,PublicInterfaceIPv4Address,PublicInterfaceIPv6Range,VPCInterface,VPCInterfaceIPv4,VPCInterfaceIPv4Address,VPCInterfaceIPv4Range,VLANInterface +//go:generate packer-sdc mapstructure-to-hcl2 -type LinodeInterface,InterfaceDefaultRoute,PublicInterface,PublicInterfaceIPv4,PublicInterfaceIPv6,PublicInterfaceIPv4Address,PublicInterfaceIPv6Range,VPCInterface,VPCInterfaceIPv4,VPCInterfaceIPv4Address,VPCInterfaceIPv4Range,VPCInterfaceIPv6,VPCInterfaceIPv6SLAAC,VPCInterfaceIPv6Range,VLANInterface package linode type LinodeInterface struct { @@ -79,7 +79,11 @@ type VPCInterface struct { // Interfaces can be configured with IPv4 addresses or ranges. IPv4 *VPCInterfaceIPv4 `mapstructure:"ipv4" required:"false"` + + // IPv6 configuration for this VPC interface. + IPv6 *VPCInterfaceIPv6 `mapstructure:"ipv6" required:"false"` } + type VPCInterfaceIPv4 struct { // IPv4 address settings for this VPC interface. Addresses []VPCInterfaceIPv4Address `mapstructure:"addresses" required:"false"` @@ -110,6 +114,27 @@ type VPCInterfaceIPv4Range struct { Range string `mapstructure:"range" required:"true"` } +type VPCInterfaceIPv6 struct { + // IPv6 SLAAC settings for this VPC interface. + SLAAC []VPCInterfaceIPv6SLAAC `mapstructure:"slaac" required:"false"` + + // IPv6 ranges for this VPC interface. + Ranges []VPCInterfaceIPv6Range `mapstructure:"ranges" required:"false"` + + // Whether the IPv6 addresses are publicly routable. + IsPublic *bool `mapstructure:"is_public" required:"false"` +} + +type VPCInterfaceIPv6SLAAC struct { + // The IPv6 SLAAC range for this VPC interface. + Range string `mapstructure:"range" required:"true"` +} + +type VPCInterfaceIPv6Range struct { + // The IPv6 range for this VPC interface. + Range string `mapstructure:"range" required:"true"` +} + type VLANInterface struct { // The VLAN's unique label. VLAN interfaces on the same Linode must have a unique `vlan_label`. VLANLabel string `mapstructure:"vlan_label" required:"true"` diff --git a/builder/linode/linode_interfaces.hcl2spec.go b/builder/linode/linode_interfaces.hcl2spec.go index 24ac5f79..b940dd24 100644 --- a/builder/linode/linode_interfaces.hcl2spec.go +++ b/builder/linode/linode_interfaces.hcl2spec.go @@ -212,6 +212,7 @@ func (*FlatVLANInterface) HCL2Spec() map[string]hcldec.Spec { type FlatVPCInterface struct { SubnetID *int `mapstructure:"subnet_id" required:"true" cty:"subnet_id" hcl:"subnet_id"` IPv4 *FlatVPCInterfaceIPv4 `mapstructure:"ipv4" required:"false" cty:"ipv4" hcl:"ipv4"` + IPv6 *FlatVPCInterfaceIPv6 `mapstructure:"ipv6" required:"false" cty:"ipv6" hcl:"ipv6"` } // FlatMapstructure returns a new FlatVPCInterface. @@ -228,6 +229,7 @@ func (*FlatVPCInterface) HCL2Spec() map[string]hcldec.Spec { s := map[string]hcldec.Spec{ "subnet_id": &hcldec.AttrSpec{Name: "subnet_id", Type: cty.Number, Required: false}, "ipv4": &hcldec.BlockSpec{TypeName: "ipv4", Nested: hcldec.ObjectSpec((*FlatVPCInterfaceIPv4)(nil).HCL2Spec())}, + "ipv6": &hcldec.BlockSpec{TypeName: "ipv6", Nested: hcldec.ObjectSpec((*FlatVPCInterfaceIPv6)(nil).HCL2Spec())}, } return s } @@ -306,3 +308,76 @@ func (*FlatVPCInterfaceIPv4Range) HCL2Spec() map[string]hcldec.Spec { } return s } + +// FlatVPCInterfaceIPv6 is an auto-generated flat version of VPCInterfaceIPv6. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatVPCInterfaceIPv6 struct { + SLAAC []FlatVPCInterfaceIPv6SLAAC `mapstructure:"slaac" required:"false" cty:"slaac" hcl:"slaac"` + Ranges []FlatVPCInterfaceIPv6Range `mapstructure:"ranges" required:"false" cty:"ranges" hcl:"ranges"` + IsPublic *bool `mapstructure:"is_public" required:"false" cty:"is_public" hcl:"is_public"` +} + +// FlatMapstructure returns a new FlatVPCInterfaceIPv6. +// FlatVPCInterfaceIPv6 is an auto-generated flat version of VPCInterfaceIPv6. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*VPCInterfaceIPv6) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatVPCInterfaceIPv6) +} + +// HCL2Spec returns the hcl spec of a VPCInterfaceIPv6. +// This spec is used by HCL to read the fields of VPCInterfaceIPv6. +// The decoded values from this spec will then be applied to a FlatVPCInterfaceIPv6. +func (*FlatVPCInterfaceIPv6) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "slaac": &hcldec.BlockListSpec{TypeName: "slaac", Nested: hcldec.ObjectSpec((*FlatVPCInterfaceIPv6SLAAC)(nil).HCL2Spec())}, + "ranges": &hcldec.BlockListSpec{TypeName: "ranges", Nested: hcldec.ObjectSpec((*FlatVPCInterfaceIPv6Range)(nil).HCL2Spec())}, + "is_public": &hcldec.AttrSpec{Name: "is_public", Type: cty.Bool, Required: false}, + } + return s +} + +// FlatVPCInterfaceIPv6Range is an auto-generated flat version of VPCInterfaceIPv6Range. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatVPCInterfaceIPv6Range struct { + Range *string `mapstructure:"range" required:"true" cty:"range" hcl:"range"` +} + +// FlatMapstructure returns a new FlatVPCInterfaceIPv6Range. +// FlatVPCInterfaceIPv6Range is an auto-generated flat version of VPCInterfaceIPv6Range. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*VPCInterfaceIPv6Range) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatVPCInterfaceIPv6Range) +} + +// HCL2Spec returns the hcl spec of a VPCInterfaceIPv6Range. +// This spec is used by HCL to read the fields of VPCInterfaceIPv6Range. +// The decoded values from this spec will then be applied to a FlatVPCInterfaceIPv6Range. +func (*FlatVPCInterfaceIPv6Range) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "range": &hcldec.AttrSpec{Name: "range", Type: cty.String, Required: false}, + } + return s +} + +// FlatVPCInterfaceIPv6SLAAC is an auto-generated flat version of VPCInterfaceIPv6SLAAC. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatVPCInterfaceIPv6SLAAC struct { + Range *string `mapstructure:"range" required:"true" cty:"range" hcl:"range"` +} + +// FlatMapstructure returns a new FlatVPCInterfaceIPv6SLAAC. +// FlatVPCInterfaceIPv6SLAAC is an auto-generated flat version of VPCInterfaceIPv6SLAAC. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*VPCInterfaceIPv6SLAAC) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatVPCInterfaceIPv6SLAAC) +} + +// HCL2Spec returns the hcl spec of a VPCInterfaceIPv6SLAAC. +// This spec is used by HCL to read the fields of VPCInterfaceIPv6SLAAC. +// The decoded values from this spec will then be applied to a FlatVPCInterfaceIPv6SLAAC. +func (*FlatVPCInterfaceIPv6SLAAC) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "range": &hcldec.AttrSpec{Name: "range", Type: cty.String, Required: false}, + } + return s +} diff --git a/builder/linode/step_create_linode.go b/builder/linode/step_create_linode.go index f479aa59..a39c48df 100644 --- a/builder/linode/step_create_linode.go +++ b/builder/linode/step_create_linode.go @@ -95,6 +95,25 @@ func flattenVPCInterface(vpc *VPCInterface) *linodego.VPCInterfaceCreateOptions Ranges: linodego.Pointer(ranges), } } + if vpc.IPv6 != nil { + slaac := make([]linodego.VPCInterfaceIPv6SLAACCreateOptions, len(vpc.IPv6.SLAAC)) + ranges := make([]linodego.VPCInterfaceIPv6RangeCreateOptions, len(vpc.IPv6.Ranges)) + for i, s := range vpc.IPv6.SLAAC { + slaac[i] = linodego.VPCInterfaceIPv6SLAACCreateOptions{ + Range: s.Range, + } + } + for i, r := range vpc.IPv6.Ranges { + ranges[i] = linodego.VPCInterfaceIPv6RangeCreateOptions{ + Range: r.Range, + } + } + result.IPv6 = &linodego.VPCInterfaceIPv6CreateOptions{ + SLAAC: linodego.Pointer(slaac), + Ranges: linodego.Pointer(ranges), + IsPublic: vpc.IPv6.IsPublic, + } + } return result } @@ -168,7 +187,7 @@ func (s *stepCreateLinode) Run(ctx context.Context, state multistep.StateBag) mu if !useCustomDisks { createOpts.RootPass = c.Comm.Password() createOpts.Image = c.Image - createOpts.SwapSize = &c.SwapSize + createOpts.SwapSize = c.SwapSize createOpts.StackScriptID = c.StackScriptID createOpts.StackScriptData = c.StackScriptData diff --git a/builder/linode/step_create_linode_test.go b/builder/linode/step_create_linode_test.go index ca953325..0503656e 100644 --- a/builder/linode/step_create_linode_test.go +++ b/builder/linode/step_create_linode_test.go @@ -55,6 +55,44 @@ func TestFlattenVPCInterface_IPv4AddressFields(t *testing.T) { } } +func TestFlattenVPCInterface_IPv6Fields(t *testing.T) { + vpc := &VPCInterface{ + SubnetID: 12345, + IPv6: &VPCInterfaceIPv6{ + SLAAC: []VPCInterfaceIPv6SLAAC{ + {Range: "2600:3c03:e000:123::/64"}, + }, + Ranges: []VPCInterfaceIPv6Range{ + {Range: "2600:3c03:e000:123:1::/64"}, + }, + IsPublic: linodego.Pointer(true), + }, + } + + got := flattenVPCInterface(vpc) + if got == nil { + t.Fatal("flattenVPCInterface() returned nil") + } + if got.IPv6 == nil { + t.Fatal("flattenVPCInterface().IPv6 returned nil") + } + if got.IPv6.SLAAC == nil || len(*got.IPv6.SLAAC) != 1 { + t.Fatalf("slaac = %v, want one slaac range", got.IPv6.SLAAC) + } + if (*got.IPv6.SLAAC)[0].Range != "2600:3c03:e000:123::/64" { + t.Fatalf("slaac range = %q, want 2600:3c03:e000:123::/64", (*got.IPv6.SLAAC)[0].Range) + } + if got.IPv6.Ranges == nil || len(*got.IPv6.Ranges) != 1 { + t.Fatalf("ranges = %v, want one ipv6 range", got.IPv6.Ranges) + } + if (*got.IPv6.Ranges)[0].Range != "2600:3c03:e000:123:1::/64" { + t.Fatalf("range = %q, want 2600:3c03:e000:123:1::/64", (*got.IPv6.Ranges)[0].Range) + } + if got.IPv6.IsPublic == nil || !*got.IPv6.IsPublic { + t.Fatalf("is_public = %v, want true", got.IPv6.IsPublic) + } +} + func TestFlattenConfigInterface_AllFields(t *testing.T) { vpcIP := &InterfaceIPv4{VPC: "10.0.0.2", NAT1To1: linodego.Pointer("198.51.100.2")} iface := Interface{ diff --git a/docs/builders/linode.mdx b/docs/builders/linode.mdx index b9824f40..f9789f22 100644 --- a/docs/builders/linode.mdx +++ b/docs/builders/linode.mdx @@ -127,6 +127,24 @@ This section outlines the fields configurable for a newer Linode interface objec @include 'builder/linode/VPCInterfaceIPv4Range-required.mdx' +##### VPC Linode Interface IPv6 configuration object (VPCInterfaceIPv6) + +###### Optional + +@include 'builder/linode/VPCInterfaceIPv6-not-required.mdx' + +##### VPC Linode Interface IPv6 SLAAC configuration object (VPCInterfaceIPv6SLAAC) + +###### Required + +@include 'builder/linode/VPCInterfaceIPv6SLAAC-required.mdx' + +##### VPC Linode Interface IPv6 Range configuration object (VPCInterfaceIPv6Range) + +###### Required + +@include 'builder/linode/VPCInterfaceIPv6Range-required.mdx' + ##### VLAN Linode Interface configuration object (VLANInterface) ###### Required