From bdf48a02ebea8d31628cef1c23c618009ec4ae8f Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Fri, 1 May 2026 10:12:30 -0400 Subject: [PATCH 1/4] Added support for SLADE and CLEO features --- builder/linode/builder_test.go | 380 +++++++++++++++++++++- builder/linode/config.go | 121 +++++-- builder/linode/config.hcl2spec.go | 6 + builder/linode/config_test.go | 9 +- builder/linode/step_create_disk_config.go | 6 +- builder/linode/step_create_linode.go | 13 +- builder/linode/step_create_linode_test.go | 2 +- go.mod | 2 + go.sum | 4 +- 9 files changed, 491 insertions(+), 52 deletions(-) diff --git a/builder/linode/builder_test.go b/builder/linode/builder_test.go index 901fa7c9..8eaf0901 100644 --- a/builder/linode/builder_test.go +++ b/builder/linode/builder_test.go @@ -13,11 +13,12 @@ import ( func testConfig() map[string]any { return map[string]any{ - "linode_token": "bar", - "region": "us-ord", - "instance_type": "g6-nanode-1", - "ssh_username": "root", - "image": "linode/arch", + "linode_token": "bar", + "region": "us-ord", + "instance_type": "g6-nanode-1", + "ssh_username": "root", + "image": "linode/arch", + "authorized_keys": []string{"ssh-rsa AAAA..."}, } } @@ -373,9 +374,11 @@ func TestBuilderPrepare_AuthorizedKeysAndUsers(t *testing.T) { var b Builder config := testConfig() - // Test optional + // Test optional - when image is specified, at least one of root_pass, authorized_keys, or authorized_users is required + // So we use root_pass as the alternative auth method delete(config, "authorized_keys") delete(config, "authorized_users") + config["root_pass"] = "testpassword123" _, warnings, err := b.Prepare(config) if len(warnings) > 0 { @@ -414,6 +417,214 @@ func TestBuilderPrepare_AuthorizedKeysAndUsers(t *testing.T) { } } +// TestBuilderPrepare_RootPassOptional tests that root_pass is optional when +// authorized_keys or authorized_users is provided for instances with an image. +func TestBuilderPrepare_RootPassOptional(t *testing.T) { + t.Run("RootPassOnlyWithImage", func(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "authorized_keys") + delete(config, "authorized_users") + config["root_pass"] = "testpassword123" + + _, _, err := b.Prepare(config) + if err != nil { + t.Fatalf("root_pass only should work: %s", err) + } + if b.config.RootPass != "testpassword123" { + t.Errorf("expected root_pass to be set, got: %s", b.config.RootPass) + } + }) + + t.Run("AuthorizedKeysOnlyWithImage", func(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "root_pass") + // authorized_keys is already set in testConfig() + + _, _, err := b.Prepare(config) + if err != nil { + t.Fatalf("authorized_keys only should work: %s", err) + } + if b.config.RootPass != "" { + t.Errorf("expected root_pass to be empty, got: %s", b.config.RootPass) + } + }) + + t.Run("AuthorizedUsersOnlyWithImage", func(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "authorized_keys") + delete(config, "root_pass") + config["authorized_users"] = []string{"testuser"} + + _, _, err := b.Prepare(config) + if err != nil { + t.Fatalf("authorized_users only should work: %s", err) + } + if b.config.RootPass != "" { + t.Errorf("expected root_pass to be empty, got: %s", b.config.RootPass) + } + }) + + t.Run("NoAuthMethodWithImage", func(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "authorized_keys") + delete(config, "authorized_users") + delete(config, "root_pass") + + _, _, err := b.Prepare(config) + if err == nil { + t.Fatal("expected error when no auth method is specified with image") + } + if !strings.Contains(err.Error(), "at least one of root_pass, authorized_keys, or authorized_users must be provided") { + t.Fatalf("expected specific error message, got: %s", err) + } + }) + + t.Run("AllAuthMethodsWithImage", func(t *testing.T) { + var b Builder + config := testConfig() + config["root_pass"] = "testpassword123" + config["authorized_keys"] = []string{"ssh-rsa AAAA..."} + config["authorized_users"] = []string{"testuser"} + + _, _, err := b.Prepare(config) + if err != nil { + t.Fatalf("all auth methods together should work: %s", err) + } + }) +} + +// TestBuilderPrepare_DiskRootPassOptional tests that root_pass is optional for +// custom disks with images when authorized_keys or authorized_users is provided. +func TestBuilderPrepare_DiskRootPassOptional(t *testing.T) { + t.Run("DiskWithRootPassOnly", func(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "image") + delete(config, "authorized_keys") + config["disk"] = []map[string]any{ + { + "label": "boot", + "size": 25000, + "image": "linode/arch", + "root_pass": "diskpassword123", + }, + } + config["config"] = []map[string]any{ + {"label": "my-config", "root_device": "/dev/sda", "devices": map[string]any{"sda": map[string]any{"disk_label": "boot"}}}, + } + + _, _, err := b.Prepare(config) + if err != nil { + t.Fatalf("disk with root_pass only should work: %s", err) + } + }) + + t.Run("DiskWithAuthorizedKeysOnly", func(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "image") + delete(config, "authorized_keys") + config["disk"] = []map[string]any{ + { + "label": "boot", + "size": 25000, + "image": "linode/arch", + "authorized_keys": []string{"ssh-rsa AAAA..."}, + }, + } + config["config"] = []map[string]any{ + {"label": "my-config", "root_device": "/dev/sda", "devices": map[string]any{"sda": map[string]any{"disk_label": "boot"}}}, + } + + _, _, err := b.Prepare(config) + if err != nil { + t.Fatalf("disk with authorized_keys only should work: %s", err) + } + }) + + t.Run("DiskWithAuthorizedUsersOnly", func(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "image") + delete(config, "authorized_keys") + config["disk"] = []map[string]any{ + { + "label": "boot", + "size": 25000, + "image": "linode/arch", + "authorized_users": []string{"testuser"}, + }, + } + config["config"] = []map[string]any{ + {"label": "my-config", "root_device": "/dev/sda", "devices": map[string]any{"sda": map[string]any{"disk_label": "boot"}}}, + } + + _, _, err := b.Prepare(config) + if err != nil { + t.Fatalf("disk with authorized_users only should work: %s", err) + } + }) + + t.Run("DiskWithNoAuthMethod", func(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "image") + delete(config, "authorized_keys") + config["disk"] = []map[string]any{ + { + "label": "boot", + "size": 25000, + "image": "linode/arch", + // No auth method specified + }, + } + config["config"] = []map[string]any{ + {"label": "my-config", "root_device": "/dev/sda", "devices": map[string]any{"sda": map[string]any{"disk_label": "boot"}}}, + } + + _, _, err := b.Prepare(config) + if err == nil { + t.Fatal("expected error when disk has image but no auth method") + } + if !strings.Contains(err.Error(), "at least one of root_pass, authorized_keys, or authorized_users must be provided") { + t.Fatalf("expected specific error message, got: %s", err) + } + }) + + t.Run("DiskWithoutImageNoAuthRequired", func(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "image") + delete(config, "authorized_keys") + config["disk"] = []map[string]any{ + { + "label": "boot", + "size": 25000, + "image": "linode/arch", + "authorized_keys": []string{"ssh-rsa AAAA..."}, + }, + { + "label": "data", + "size": 10000, + "filesystem": "ext4", + // No image, so no auth method required + }, + } + config["config"] = []map[string]any{ + {"label": "my-config", "root_device": "/dev/sda", "devices": map[string]any{"sda": map[string]any{"disk_label": "boot"}, "sdb": map[string]any{"disk_label": "data"}}}, + } + + _, _, err := b.Prepare(config) + if err != nil { + t.Fatalf("disk without image should not require auth method: %s", err) + } + }) +} + func TestBuilderPrepare_PrivateIP(t *testing.T) { var b Builder config := testConfig() @@ -767,6 +978,136 @@ func TestBuilderPrepare_ImageShareGroupIDs(t *testing.T) { } } +func TestBuilderPrepare_BootSizeAndKernel(t *testing.T) { + t.Run("DefaultsAreNilAndEmpty", func(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "boot_size") + delete(config, "kernel") + + _, warnings, err := b.Prepare(config) + if len(warnings) > 0 { + t.Fatalf("bad: %#v", warnings) + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if b.config.BootSize != nil { + t.Errorf("expected boot_size to be nil, got %v", b.config.BootSize) + } + if b.config.Kernel != "" { + t.Errorf("expected kernel to be empty, got %v", b.config.Kernel) + } + }) + + t.Run("BootSizeSet", func(t *testing.T) { + var b Builder + config := testConfig() + expectedBootSize := 20000 + config["boot_size"] = expectedBootSize + + _, warnings, err := b.Prepare(config) + if len(warnings) > 0 { + t.Fatalf("bad: %#v", warnings) + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if b.config.BootSize == nil || *b.config.BootSize != expectedBootSize { + t.Errorf("expected boot_size to be %d, got %v", expectedBootSize, b.config.BootSize) + } + }) + + t.Run("KernelSet", func(t *testing.T) { + var b Builder + config := testConfig() + expectedKernel := "linode/latest-64bit" + config["kernel"] = expectedKernel + + _, warnings, err := b.Prepare(config) + if len(warnings) > 0 { + t.Fatalf("bad: %#v", warnings) + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if b.config.Kernel != expectedKernel { + t.Errorf("expected kernel to be %s, got %s", expectedKernel, b.config.Kernel) + } + }) + + t.Run("BothSet", func(t *testing.T) { + var b Builder + config := testConfig() + expectedBootSize := 15000 + expectedKernel := "linode/grub2" + config["boot_size"] = expectedBootSize + config["kernel"] = expectedKernel + + _, warnings, err := b.Prepare(config) + if len(warnings) > 0 { + t.Fatalf("bad: %#v", warnings) + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if b.config.BootSize == nil || *b.config.BootSize != expectedBootSize { + t.Errorf("expected boot_size to be %d, got %v", expectedBootSize, b.config.BootSize) + } + if b.config.Kernel != expectedKernel { + t.Errorf("expected kernel to be %s, got %s", expectedKernel, b.config.Kernel) + } + }) + + t.Run("BootSizeNotAllowedWithCustomDisks", func(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "image") + delete(config, "authorized_keys") + config["boot_size"] = 20000 + config["disk"] = []map[string]any{ + {"label": "boot", "size": 25000, "image": "linode/arch", "authorized_keys": []string{"ssh-rsa AAAA..."}}, + } + config["config"] = []map[string]any{ + {"label": "my-config", "root_device": "/dev/sda", "devices": map[string]any{"sda": map[string]any{"disk_label": "boot"}}}, + } + + _, _, err := b.Prepare(config) + if err == nil { + t.Fatal("expected error when boot_size is specified with custom disks") + } + if !strings.Contains(err.Error(), "boot_size cannot be specified when using custom disks") { + t.Fatalf("expected specific error message, got: %s", err) + } + }) + + t.Run("KernelNotAllowedWithCustomDisks", func(t *testing.T) { + var b Builder + config := testConfig() + delete(config, "image") + delete(config, "authorized_keys") + config["kernel"] = "linode/latest-64bit" + config["disk"] = []map[string]any{ + {"label": "boot", "size": 25000, "image": "linode/arch", "authorized_keys": []string{"ssh-rsa AAAA..."}}, + } + config["config"] = []map[string]any{ + {"label": "my-config", "root_device": "/dev/sda", "devices": map[string]any{"sda": map[string]any{"disk_label": "boot"}}}, + } + + _, _, err := b.Prepare(config) + if err == nil { + t.Fatal("expected error when kernel is specified with custom disks") + } + if !strings.Contains(err.Error(), "kernel cannot be specified when using custom disks") { + t.Fatalf("expected specific error message, got: %s", err) + } + }) +} + func TestBuilderPrepare_CustomDisks(t *testing.T) { var b Builder config := testConfig() @@ -774,10 +1115,11 @@ func TestBuilderPrepare_CustomDisks(t *testing.T) { // Test with custom disk config["disk"] = []map[string]any{ { - "label": "boot", - "size": 25000, - "image": "linode/arch", - "filesystem": "ext4", + "label": "boot", + "size": 25000, + "image": "linode/arch", + "filesystem": "ext4", + "authorized_keys": []string{"ssh-rsa AAAA..."}, }, { "label": "swap", @@ -804,7 +1146,9 @@ func TestBuilderPrepare_CustomDisks(t *testing.T) { } // When using custom disks, image should not be required at top level + // Also remove top-level authorized_keys since it's not allowed with custom disks delete(config, "image") + delete(config, "authorized_keys") _, warnings, err := b.Prepare(config) if len(warnings) > 0 { @@ -859,14 +1203,16 @@ func TestBuilderPrepare_CustomConfig(t *testing.T) { config["disk"] = []map[string]any{ { - "label": "boot", - "size": 25000, - "image": "linode/arch", + "label": "boot", + "size": 25000, + "image": "linode/arch", + "authorized_keys": []string{"ssh-rsa AAAA..."}, }, } - // When using custom disks, image should not be required at top level + // When using custom disks, image and authorized_keys should not be at top level delete(config, "image") + delete(config, "authorized_keys") _, warnings, err := b.Prepare(config) if len(warnings) > 0 { @@ -1117,12 +1463,13 @@ func TestBuilderPrepare_CustomDisksValidation(t *testing.T) { var b Builder config := testConfig() delete(config, "image") + delete(config, "authorized_keys") // linode_interface should be ALLOWED with custom disks config["linode_interface"] = []map[string]any{ {"public": map[string]any{}}, } config["disk"] = []map[string]any{ - {"label": "boot", "size": 25000, "image": "linode/arch"}, + {"label": "boot", "size": 25000, "image": "linode/arch", "authorized_keys": []string{"ssh-rsa AAAA..."}}, } config["config"] = []map[string]any{ {"label": "my-config", "root_device": "/dev/sda", "devices": map[string]any{"sda": map[string]any{"disk_label": "boot"}}}, @@ -1138,8 +1485,9 @@ func TestBuilderPrepare_CustomDisksValidation(t *testing.T) { var b Builder config := testConfig() delete(config, "image") + delete(config, "authorized_keys") config["disk"] = []map[string]any{ - {"label": "boot", "size": 25000, "image": "linode/arch"}, + {"label": "boot", "size": 25000, "image": "linode/arch", "authorized_keys": []string{"ssh-rsa AAAA..."}}, } config["config"] = []map[string]any{ {"label": "my-config", "devices": map[string]any{"sda": map[string]any{"disk_label": "boot"}}}, diff --git a/builder/linode/config.go b/builder/linode/config.go index fc0d90ec..e850faaf 100644 --- a/builder/linode/config.go +++ b/builder/linode/config.go @@ -4,8 +4,6 @@ package linode import ( - "crypto/rand" - "encoding/base64" "errors" "fmt" "os" @@ -44,13 +42,18 @@ type Disk struct { // when the Linode is offline and may take some time. Size int `mapstructure:"size" required:"true"` - // An Image ID to deploy the Linode Disk from. If provided, root_pass is required. + // An Image ID to deploy the Linode Disk from. If provided, + // at least one of root_pass, authorized_keys, or authorized_users + // must be provided to ensure access. Image string `mapstructure:"image" required:"false"` // The filesystem for the disk. Valid values are raw, swap, ext3, ext4, initrd. // Defaults to ext4. Filesystem string `mapstructure:"filesystem" required:"false"` + // The root password for this disk when deploying from an image. + RootPass string `mapstructure:"root_pass" required:"false"` + // A list of public SSH keys to be installed on the disk as the root user's // ~/.ssh/authorized_keys file. AuthorizedKeys []string `mapstructure:"authorized_keys" required:"false"` @@ -290,13 +293,23 @@ type Config struct { // The disk size (MiB) allocated for swap space. SwapSize *int `mapstructure:"swap_size" required:"false"` + // The size (MiB) of the primary boot disk. Any remaining disk space beyond + // the boot disk and swap partition is left unallocated. If not specified, + // the boot disk will use all available space after swap. + BootSize *int `mapstructure:"boot_size" required:"false"` + + // The kernel to boot the instance with. This can be a kernel ID such as + // "linode/latest-64bit" or "linode/grub2". See the available kernels at + // https://api.linode.com/v4/linode/kernels. + Kernel string `mapstructure:"kernel" required:"false"` + // If true, the created Linode will have private networking enabled and assigned // a private IPv4 address. PrivateIP bool `mapstructure:"private_ip" required:"false"` // The root password of the Linode instance for building the image. Please note that when - // you create a new Linode instance with a private image, you will be required to setup a - // new root password. + // you create a new Linode instance with an image, at least one of root_pass, + // authorized_keys, or authorized_users must be provided RootPass string `mapstructure:"root_pass" required:"false"` // The name of the resulting image that will appear @@ -560,16 +573,6 @@ func (c *Config) getBootDiskLabel() (string, error) { return device.DiskLabel, nil } -func createRandomRootPassword() (string, error) { - rawRootPass := make([]byte, 50) - _, err := rand.Read(rawRootPass) - if err != nil { - return "", fmt.Errorf("failed to generate random password") - } - rootPass := base64.StdEncoding.EncodeToString(rawRootPass) - return rootPass, nil -} - func (c *Config) Prepare(raws ...any) ([]string, error) { if err := config.Decode(c, &config.DecodeOpts{ Interpolate: true, @@ -613,14 +616,6 @@ func (c *Config) Prepare(raws ...any) ([]string, error) { } } - if c.RootPass == "" { - var err error - c.RootPass, err = createRandomRootPassword() - if err != nil { - errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("unable to generate root_pass: %s", err)) - } - } - if c.StateTimeout == 0 { // Default to 5 minute timeouts waiting for state change c.StateTimeout = 5 * time.Minute @@ -631,11 +626,79 @@ func (c *Config) Prepare(raws ...any) ([]string, error) { c.ImageCreateTimeout = 10 * time.Minute } + if strings.TrimSpace(c.RootPass) != "" { + c.Comm.SSHPassword = c.RootPass + } + if es := c.Comm.Prepare(&c.ctx); len(es) > 0 { errs = packersdk.MultiErrorAppend(errs, es...) } - c.Comm.SSHPassword = c.RootPass + // When creating a Linode from an image, at least one of root_pass, authorized_keys, or authorized_users + // must be provided to ensure access to the instance + if c.Image != "" { + hasRootPass := strings.TrimSpace(c.RootPass) != "" + hasKeys := len(c.AuthorizedKeys) > 0 + hasUsers := len(c.AuthorizedUsers) > 0 + + if !hasRootPass && !hasKeys && !hasUsers { + errs = packersdk.MultiErrorAppend( + errs, + fmt.Errorf( + "when image is specified, at least one of root_pass, authorized_keys, or authorized_users must be provided", + ), + ) + } + } + + for _, d := range c.Disks { + if strings.TrimSpace(d.Image) == "" { + continue + } + + hasRootPass := strings.TrimSpace(d.RootPass) != "" + hasKeys := len(d.AuthorizedKeys) > 0 + hasUsers := len(d.AuthorizedUsers) > 0 + + if !hasRootPass && !hasKeys && !hasUsers { + errs = packersdk.MultiErrorAppend( + errs, + fmt.Errorf( + "disk %q: when image is specified, at least one of root_pass, authorized_keys, or authorized_users must be provided", + d.Label, + ), + ) + } + } + + bootLabel, err := c.getBootDiskLabel() + if err == nil { + for _, d := range c.Disks { + if d.Label != bootLabel { + continue + } + + if strings.TrimSpace(d.Image) == "" { + break + } + + hasRootPass := strings.TrimSpace(d.RootPass) != "" + hasKeys := len(d.AuthorizedKeys) > 0 + hasUsers := len(d.AuthorizedUsers) > 0 + + if !hasRootPass && !hasKeys && !hasUsers { + errs = packersdk.MultiErrorAppend( + errs, + fmt.Errorf( + "boot disk %q must define root_pass, authorized_keys, or authorized_users", + d.Label, + ), + ) + } + + break + } + } if c.PersonalAccessToken == "" { // Required configurations that will display errors if not set @@ -725,6 +788,16 @@ func (c *Config) Prepare(raws ...any) ([]string, error) { errs, errors.New("swap_size cannot be specified when using custom disks (create a swap disk instead)")) } + if c.BootSize != nil { + errs = packersdk.MultiErrorAppend( + errs, errors.New("boot_size cannot be specified when using custom disks (specify size in disk blocks instead)")) + } + + if c.Kernel != "" { + errs = packersdk.MultiErrorAppend( + errs, errors.New("kernel cannot be specified when using custom disks (specify in config blocks instead)")) + } + if c.StackScriptID > 0 { errs = packersdk.MultiErrorAppend( errs, errors.New("stackscript_id cannot be specified when using custom disks (specify in disk blocks instead)")) diff --git a/builder/linode/config.hcl2spec.go b/builder/linode/config.hcl2spec.go index 0db49e03..fa534769 100644 --- a/builder/linode/config.hcl2spec.go +++ b/builder/linode/config.hcl2spec.go @@ -79,6 +79,8 @@ type FlatConfig struct { Tags []string `mapstructure:"instance_tags" required:"false" cty:"instance_tags" hcl:"instance_tags"` Image *string `mapstructure:"image" required:"false" cty:"image" hcl:"image"` SwapSize *int `mapstructure:"swap_size" required:"false" cty:"swap_size" hcl:"swap_size"` + BootSize *int `mapstructure:"boot_size" required:"false" cty:"boot_size" hcl:"boot_size"` + Kernel *string `mapstructure:"kernel" required:"false" cty:"kernel" hcl:"kernel"` PrivateIP *bool `mapstructure:"private_ip" required:"false" cty:"private_ip" hcl:"private_ip"` RootPass *string `mapstructure:"root_pass" required:"false" cty:"root_pass" hcl:"root_pass"` ImageLabel *string `mapstructure:"image_label" required:"false" cty:"image_label" hcl:"image_label"` @@ -178,6 +180,8 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "instance_tags": &hcldec.AttrSpec{Name: "instance_tags", Type: cty.List(cty.String), Required: false}, "image": &hcldec.AttrSpec{Name: "image", Type: cty.String, Required: false}, "swap_size": &hcldec.AttrSpec{Name: "swap_size", Type: cty.Number, Required: false}, + "boot_size": &hcldec.AttrSpec{Name: "boot_size", Type: cty.Number, Required: false}, + "kernel": &hcldec.AttrSpec{Name: "kernel", Type: cty.String, Required: false}, "private_ip": &hcldec.AttrSpec{Name: "private_ip", Type: cty.Bool, Required: false}, "root_pass": &hcldec.AttrSpec{Name: "root_pass", Type: cty.String, Required: false}, "image_label": &hcldec.AttrSpec{Name: "image_label", Type: cty.String, Required: false}, @@ -205,6 +209,7 @@ type FlatDisk struct { Size *int `mapstructure:"size" required:"true" cty:"size" hcl:"size"` Image *string `mapstructure:"image" required:"false" cty:"image" hcl:"image"` Filesystem *string `mapstructure:"filesystem" required:"false" cty:"filesystem" hcl:"filesystem"` + RootPass *string `mapstructure:"root_pass" required:"false" cty:"root_pass" hcl:"root_pass"` AuthorizedKeys []string `mapstructure:"authorized_keys" required:"false" cty:"authorized_keys" hcl:"authorized_keys"` AuthorizedUsers []string `mapstructure:"authorized_users" required:"false" cty:"authorized_users" hcl:"authorized_users"` StackscriptID *int `mapstructure:"stackscript_id" required:"false" cty:"stackscript_id" hcl:"stackscript_id"` @@ -227,6 +232,7 @@ func (*FlatDisk) HCL2Spec() map[string]hcldec.Spec { "size": &hcldec.AttrSpec{Name: "size", Type: cty.Number, Required: false}, "image": &hcldec.AttrSpec{Name: "image", Type: cty.String, Required: false}, "filesystem": &hcldec.AttrSpec{Name: "filesystem", Type: cty.String, Required: false}, + "root_pass": &hcldec.AttrSpec{Name: "root_pass", Type: cty.String, Required: false}, "authorized_keys": &hcldec.AttrSpec{Name: "authorized_keys", Type: cty.List(cty.String), Required: false}, "authorized_users": &hcldec.AttrSpec{Name: "authorized_users", Type: cty.List(cty.String), Required: false}, "stackscript_id": &hcldec.AttrSpec{Name: "stackscript_id", Type: cty.Number, Required: false}, diff --git a/builder/linode/config_test.go b/builder/linode/config_test.go index 2fc60632..62c70667 100644 --- a/builder/linode/config_test.go +++ b/builder/linode/config_test.go @@ -22,10 +22,11 @@ func TestPrepare(t *testing.T) { ctx: interpolate.Context{}, Comm: communicator.Config{SSH: data}, - Region: "us-ord", - InstanceType: "g6-standard-1", - Image: "linode/debian12", - ImageRegions: []string{"us-ord", "us-mia", "us-lax"}, + Region: "us-ord", + InstanceType: "g6-standard-1", + Image: "linode/debian12", + ImageRegions: []string{"us-ord", "us-mia", "us-lax"}, + AuthorizedKeys: []string{"ssh-rsa AAAA..."}, } warnings, err := config.Prepare() diff --git a/builder/linode/step_create_disk_config.go b/builder/linode/step_create_disk_config.go index a2b8162a..8b88f5ca 100644 --- a/builder/linode/step_create_disk_config.go +++ b/builder/linode/step_create_disk_config.go @@ -24,6 +24,7 @@ func flattenDisk(d Disk) linodego.InstanceDiskCreateOptions { Size: d.Size, Image: d.Image, Filesystem: d.Filesystem, + RootPass: d.RootPass, AuthorizedKeys: d.AuthorizedKeys, AuthorizedUsers: d.AuthorizedUsers, StackscriptID: d.StackscriptID, @@ -288,9 +289,8 @@ func (s *stepCreateDiskConfig) Run(ctx context.Context, state multistep.StateBag } } - if diskOpts.RootPass == "" && diskOpts.Image != "" { - diskOpts.RootPass = c.Comm.Password() - } + // Note: Each disk with an image must define its own auth method (root_pass, authorized_keys, or authorized_users) + // This is validated in config.go - we don't fall back to any default here disk, err := s.client.CreateInstanceDisk(ctx, instance.ID, diskOpts) if err != nil { diff --git a/builder/linode/step_create_linode.go b/builder/linode/step_create_linode.go index c9a43fee..e0fbee41 100644 --- a/builder/linode/step_create_linode.go +++ b/builder/linode/step_create_linode.go @@ -112,7 +112,9 @@ func flattenVLANInterface(vlan *VLANInterface) *linodego.VLANInterface { } func flattenLinodeInterface(li LinodeInterface) (opts linodego.LinodeInterfaceCreateOptions) { - opts.FirewallID = li.FirewallID + if li.FirewallID != nil { + opts.FirewallID = &li.FirewallID + } if li.DefaultRoute != nil { opts.DefaultRoute = &linodego.InterfaceDefaultRoute{ @@ -166,12 +168,19 @@ func (s *stepCreateLinode) Run(ctx context.Context, state multistep.StateBag) mu // Only set image-related options when NOT using custom disks if !useCustomDisks { - createOpts.RootPass = c.Comm.Password() + // Use c.RootPass directly instead of c.Comm.Password() to respect the user's choice + // If root_pass is not provided, the API will accept it when authorized_keys or authorized_users is set + createOpts.RootPass = c.RootPass createOpts.Image = c.Image createOpts.SwapSize = c.SwapSize + createOpts.BootSize = c.BootSize createOpts.StackScriptID = c.StackScriptID createOpts.StackScriptData = c.StackScriptData + if c.Kernel != "" { + createOpts.Kernel = &c.Kernel + } + if pubKey := string(c.Comm.SSHPublicKey); pubKey != "" { createOpts.AuthorizedKeys = append(createOpts.AuthorizedKeys, pubKey) } diff --git a/builder/linode/step_create_linode_test.go b/builder/linode/step_create_linode_test.go index 3d25bfea..ca953325 100644 --- a/builder/linode/step_create_linode_test.go +++ b/builder/linode/step_create_linode_test.go @@ -151,7 +151,7 @@ func TestFlattenLinodeInterface_AllFields(t *testing.T) { } got := flattenLinodeInterface(li) - if got.FirewallID == nil || *got.FirewallID != 123 { + if got.FirewallID == nil || *got.FirewallID == nil || **got.FirewallID != 123 { t.Fatalf("firewall_id = %v, want 123", got.FirewallID) } if got.DefaultRoute == nil || got.DefaultRoute.IPv4 == nil || !*got.DefaultRoute.IPv4 { diff --git a/go.mod b/go.mod index c8bf7982..7fb4906c 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,8 @@ require ( golang.org/x/oauth2 v0.36.0 ) +replace github.com/linode/linodego => github.com/psnoch-akamai/linodego v0.0.0-20260429094800-f218e562c953 + require ( cel.dev/expr v0.25.1 // indirect cloud.google.com/go v0.123.0 // indirect diff --git a/go.sum b/go.sum index 6a2f86bd..e62f46b7 100644 --- a/go.sum +++ b/go.sum @@ -287,8 +287,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/linode/linodego v1.67.0 h1:pomhFuuCCJI4N6emtB9027h1yXHY2/MIT0hwHEFwvq4= -github.com/linode/linodego v1.67.0/go.mod h1:+9mbdu0P3WMRCl0QbVfiFavR+Iel7TCRDJk3nInyx14= github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 h1:2ZKn+w/BJeL43sCxI2jhPLRv73oVVOjEKZjKkflyqxg= github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= github.com/masterzen/winrm v0.0.0-20250927112105-5f8e6c707321 h1:AKIJL2PfBX2uie0Mn5pxtG1+zut3hAVMZbRfoXecFzI= @@ -369,6 +367,8 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/psnoch-akamai/linodego v0.0.0-20260429094800-f218e562c953 h1:5J7qQe1LyW6AmQE1y6fXJDNcbKJpp2fdaxRjrJd903w= +github.com/psnoch-akamai/linodego v0.0.0-20260429094800-f218e562c953/go.mod h1:12ykGs9qsvxE+OU3SXuW2w+DTruWF35FPlXC7gGk2tU= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= From 0511d346143a0b6315a369b523694d9796d0b816 Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Fri, 1 May 2026 11:03:50 -0400 Subject: [PATCH 2/4] Ran make generate --- .web-docs/components/builder/linode/README.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/.web-docs/components/builder/linode/README.md b/.web-docs/components/builder/linode/README.md index e588e301..8cfc140a 100644 --- a/.web-docs/components/builder/linode/README.md +++ b/.web-docs/components/builder/linode/README.md @@ -90,12 +90,20 @@ can also be supplied to override the typical auto-generated key: - `swap_size` (\*int) - The disk size (MiB) allocated for swap space. +- `boot_size` (\*int) - The size (MiB) of the primary boot disk. Any remaining disk space beyond + the boot disk and swap partition is left unallocated. If not specified, + the boot disk will use all available space after swap. + +- `kernel` (string) - The kernel to boot the instance with. This can be a kernel ID such as + "linode/latest-64bit" or "linode/grub2". See the available kernels at + https://api.linode.com/v4/linode/kernels. + - `private_ip` (bool) - If true, the created Linode will have private networking enabled and assigned a private IPv4 address. - `root_pass` (string) - The root password of the Linode instance for building the image. Please note that when - you create a new Linode instance with a private image, you will be required to setup a - new root password. + you create a new Linode instance with an image, at least one of root_pass, + authorized_keys, or authorized_users must be provided - `image_label` (string) - The name of the resulting image that will appear in your account. Defaults to `packer-{{timestamp}}` (see [configuration @@ -456,11 +464,15 @@ The SSH public key from the communicator configuration will be automatically add -- `image` (string) - An Image ID to deploy the Linode Disk from. If provided, root_pass is required. +- `image` (string) - An Image ID to deploy the Linode Disk from. If provided, + at least one of root_pass, authorized_keys, or authorized_users + must be provided to ensure access. - `filesystem` (string) - The filesystem for the disk. Valid values are raw, swap, ext3, ext4, initrd. Defaults to ext4. +- `root_pass` (string) - The root password for this disk when deploying from an image. + - `authorized_keys` ([]string) - A list of public SSH keys to be installed on the disk as the root user's ~/.ssh/authorized_keys file. From b90d64162298d883a405084ca55e93e4b897e052 Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Fri, 1 May 2026 15:26:52 -0400 Subject: [PATCH 3/4] Address CoPilot suggestions --- builder/linode/builder_test.go | 91 +++++++++++++++++++++++++++++++--- builder/linode/config.go | 25 ++++++++-- 2 files changed, 105 insertions(+), 11 deletions(-) diff --git a/builder/linode/builder_test.go b/builder/linode/builder_test.go index d65a768a..4eb66e97 100644 --- a/builder/linode/builder_test.go +++ b/builder/linode/builder_test.go @@ -467,16 +467,34 @@ func TestBuilderPrepare_RootPassOptional(t *testing.T) { } }) - t.Run("NoAuthMethodWithImage", func(t *testing.T) { + t.Run("NoAuthMethodWithImage_AutoGeneratedKey", func(t *testing.T) { + // When no explicit auth method is provided and no ssh_private_key_file, + // Packer will auto-generate an SSH key, which is acceptable var b Builder config := testConfig() delete(config, "authorized_keys") delete(config, "authorized_users") delete(config, "root_pass") + _, _, err := b.Prepare(config) + if err != nil { + t.Fatalf("should pass when Packer can auto-generate SSH key: %s", err) + } + }) + + t.Run("NoAuthMethodWithImage_PrivateKeyFile", func(t *testing.T) { + // When ssh_private_key_file is provided, Packer won't auto-generate a key, + // so at least one explicit auth method must be provided + var b Builder + config := testConfig() + delete(config, "authorized_keys") + delete(config, "authorized_users") + delete(config, "root_pass") + config["ssh_private_key_file"] = "/path/to/key" + _, _, err := b.Prepare(config) if err == nil { - t.Fatal("expected error when no auth method is specified with image") + t.Fatal("expected error when ssh_private_key_file is set but no auth method is specified with image") } if !strings.Contains(err.Error(), "at least one of root_pass, authorized_keys, or authorized_users must be provided") { t.Fatalf("expected specific error message, got: %s", err) @@ -569,11 +587,37 @@ func TestBuilderPrepare_DiskRootPassOptional(t *testing.T) { } }) - t.Run("DiskWithNoAuthMethod", func(t *testing.T) { + t.Run("BootDiskWithNoAuthMethod_AutoGeneratedKey", func(t *testing.T) { + // Boot disk can rely on auto-generated SSH key when no ssh_private_key_file is set + var b Builder + config := testConfig() + delete(config, "image") + delete(config, "authorized_keys") + config["disk"] = []map[string]any{ + { + "label": "boot", + "size": 25000, + "image": "linode/arch", + // No auth method specified - will use auto-generated SSH key + }, + } + config["config"] = []map[string]any{ + {"label": "my-config", "root_device": "/dev/sda", "devices": map[string]any{"sda": map[string]any{"disk_label": "boot"}}}, + } + + _, _, err := b.Prepare(config) + if err != nil { + t.Fatalf("boot disk should pass when Packer can auto-generate SSH key: %s", err) + } + }) + + t.Run("BootDiskWithNoAuthMethod_PrivateKeyFile", func(t *testing.T) { + // When ssh_private_key_file is set, boot disk needs explicit auth var b Builder config := testConfig() delete(config, "image") delete(config, "authorized_keys") + config["ssh_private_key_file"] = "/path/to/key" config["disk"] = []map[string]any{ { "label": "boot", @@ -588,10 +632,11 @@ func TestBuilderPrepare_DiskRootPassOptional(t *testing.T) { _, _, err := b.Prepare(config) if err == nil { - t.Fatal("expected error when disk has image but no auth method") + t.Fatal("expected error when ssh_private_key_file is set but boot disk has no auth method") } - if !strings.Contains(err.Error(), "at least one of root_pass, authorized_keys, or authorized_users must be provided") { - t.Fatalf("expected specific error message, got: %s", err) + errStr := err.Error() + if !strings.Contains(errStr, "root_pass, authorized_keys, or authorized_users") { + t.Fatalf("expected specific error message, got: %s", errStr) } }) @@ -623,6 +668,40 @@ func TestBuilderPrepare_DiskRootPassOptional(t *testing.T) { t.Fatalf("disk without image should not require auth method: %s", err) } }) + + t.Run("NonBootDiskWithImage_NeedsExplicitAuth", func(t *testing.T) { + // Non-boot disks with an image still need explicit auth (auto-generated key only applies to boot disk) + var b Builder + config := testConfig() + delete(config, "image") + delete(config, "authorized_keys") + config["disk"] = []map[string]any{ + { + "label": "boot", + "size": 25000, + "image": "linode/arch", + "authorized_keys": []string{"ssh-rsa AAAA..."}, + }, + { + "label": "other", + "size": 10000, + "image": "linode/debian11", + // No auth method - this should fail even though Packer auto-generates key + // because auto-generated key is only added to boot disk + }, + } + config["config"] = []map[string]any{ + {"label": "my-config", "root_device": "/dev/sda", "devices": map[string]any{"sda": map[string]any{"disk_label": "boot"}, "sdb": map[string]any{"disk_label": "other"}}}, + } + + _, _, err := b.Prepare(config) + if err == nil { + t.Fatal("expected error when non-boot disk with image has no auth method") + } + if !strings.Contains(err.Error(), "at least one of root_pass, authorized_keys, or authorized_users must be provided") { + t.Fatalf("expected specific error message, got: %s", err) + } + }) } func TestBuilderPrepare_PrivateIP(t *testing.T) { diff --git a/builder/linode/config.go b/builder/linode/config.go index e850faaf..66d0ea1e 100644 --- a/builder/linode/config.go +++ b/builder/linode/config.go @@ -635,13 +635,17 @@ func (c *Config) Prepare(raws ...any) ([]string, error) { } // When creating a Linode from an image, at least one of root_pass, authorized_keys, or authorized_users - // must be provided to ensure access to the instance + // must be provided to ensure access to the instance. + // Note: If no SSHPrivateKeyFile is provided, Packer will auto-generate an SSH key pair during the build, + // which will be added to authorized_keys on the instance, satisfying this requirement. if c.Image != "" { hasRootPass := strings.TrimSpace(c.RootPass) != "" hasKeys := len(c.AuthorizedKeys) > 0 hasUsers := len(c.AuthorizedUsers) > 0 + // If no private key file is specified, Packer will auto-generate an SSH key + willAutoGenerateKey := c.Comm.SSHPrivateKeyFile == "" - if !hasRootPass && !hasKeys && !hasUsers { + if !hasRootPass && !hasKeys && !hasUsers && !willAutoGenerateKey { errs = packersdk.MultiErrorAppend( errs, fmt.Errorf( @@ -651,11 +655,20 @@ func (c *Config) Prepare(raws ...any) ([]string, error) { } } + // Get boot disk label for validation (if custom disks are configured) + bootLabel, bootLabelErr := c.getBootDiskLabel() + + // Validate non-boot disks: they don't get the auto-generated SSH key, so they need explicit auth for _, d := range c.Disks { if strings.TrimSpace(d.Image) == "" { continue } + // Skip boot disk - it's validated separately below + if bootLabelErr == nil && d.Label == bootLabel { + continue + } + hasRootPass := strings.TrimSpace(d.RootPass) != "" hasKeys := len(d.AuthorizedKeys) > 0 hasUsers := len(d.AuthorizedUsers) > 0 @@ -671,8 +684,8 @@ func (c *Config) Prepare(raws ...any) ([]string, error) { } } - bootLabel, err := c.getBootDiskLabel() - if err == nil { + // Validate boot disk: it gets the auto-generated SSH key appended (if no ssh_private_key_file) + if bootLabelErr == nil { for _, d := range c.Disks { if d.Label != bootLabel { continue @@ -685,8 +698,10 @@ func (c *Config) Prepare(raws ...any) ([]string, error) { hasRootPass := strings.TrimSpace(d.RootPass) != "" hasKeys := len(d.AuthorizedKeys) > 0 hasUsers := len(d.AuthorizedUsers) > 0 + // If no private key file is specified, Packer will auto-generate an SSH key + willAutoGenerateKey := c.Comm.SSHPrivateKeyFile == "" - if !hasRootPass && !hasKeys && !hasUsers { + if !hasRootPass && !hasKeys && !hasUsers && !willAutoGenerateKey { errs = packersdk.MultiErrorAppend( errs, fmt.Errorf( From de15ff519c9735f8de34796e879c3737469c284d Mon Sep 17 00:00:00 2001 From: ezilber-akamai Date: Thu, 7 May 2026 15:50:11 -0400 Subject: [PATCH 4/4] Point at least linodego release --- builder/linode/step_create_linode.go | 2 +- builder/linode/step_create_linode_test.go | 2 +- go.mod | 6 ++---- go.sum | 8 ++++---- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/builder/linode/step_create_linode.go b/builder/linode/step_create_linode.go index df8b027b..e7202d80 100644 --- a/builder/linode/step_create_linode.go +++ b/builder/linode/step_create_linode.go @@ -132,7 +132,7 @@ func flattenVLANInterface(vlan *VLANInterface) *linodego.VLANInterface { func flattenLinodeInterface(li LinodeInterface) (opts linodego.LinodeInterfaceCreateOptions) { if li.FirewallID != nil { - opts.FirewallID = &li.FirewallID + opts.FirewallID = li.FirewallID } if li.DefaultRoute != nil { diff --git a/builder/linode/step_create_linode_test.go b/builder/linode/step_create_linode_test.go index 0503656e..f95a958a 100644 --- a/builder/linode/step_create_linode_test.go +++ b/builder/linode/step_create_linode_test.go @@ -189,7 +189,7 @@ func TestFlattenLinodeInterface_AllFields(t *testing.T) { } got := flattenLinodeInterface(li) - if got.FirewallID == nil || *got.FirewallID == nil || **got.FirewallID != 123 { + if got.FirewallID == nil || *got.FirewallID != 123 { t.Fatalf("firewall_id = %v, want 123", got.FirewallID) } if got.DefaultRoute == nil || got.DefaultRoute.IPv4 == nil || !*got.DefaultRoute.IPv4 { diff --git a/go.mod b/go.mod index ff93669c..e50a8998 100644 --- a/go.mod +++ b/go.mod @@ -7,15 +7,13 @@ toolchain go1.25.7 require ( github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/packer-plugin-sdk v0.6.7 - github.com/linode/linodego v1.68.0 + github.com/linode/linodego v1.69.0 github.com/mitchellh/mapstructure v1.5.0 github.com/zclconf/go-cty v1.16.3 golang.org/x/crypto v0.50.0 golang.org/x/oauth2 v0.36.0 ) -replace github.com/linode/linodego => github.com/psnoch-akamai/linodego v0.0.0-20260429094800-f218e562c953 - require ( cel.dev/expr v0.25.1 // indirect cloud.google.com/go v0.123.0 // indirect @@ -149,7 +147,7 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect google.golang.org/grpc v1.79.3 // indirect google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/ini.v1 v1.67.1 // indirect + gopkg.in/ini.v1 v1.67.2 // indirect ) // Temporary replacement for issue causing releases to fail (see: #379) diff --git a/go.sum b/go.sum index c86eeef7..489c5e98 100644 --- a/go.sum +++ b/go.sum @@ -287,6 +287,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/linode/linodego v1.69.0 h1:5Qsv6kwoTwwOBETfgietEg5UcaKg1bQEN0DDYc2wIeM= +github.com/linode/linodego v1.69.0/go.mod h1:QPzvRKCy9oz1lRaIACPRORoAI9AAfKKg2AR5mXujoB0= github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 h1:2ZKn+w/BJeL43sCxI2jhPLRv73oVVOjEKZjKkflyqxg= github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= github.com/masterzen/winrm v0.0.0-20250927112105-5f8e6c707321 h1:AKIJL2PfBX2uie0Mn5pxtG1+zut3hAVMZbRfoXecFzI= @@ -367,8 +369,6 @@ github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8b github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/psnoch-akamai/linodego v0.0.0-20260429094800-f218e562c953 h1:5J7qQe1LyW6AmQE1y6fXJDNcbKJpp2fdaxRjrJd903w= -github.com/psnoch-akamai/linodego v0.0.0-20260429094800-f218e562c953/go.mod h1:12ykGs9qsvxE+OU3SXuW2w+DTruWF35FPlXC7gGk2tU= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= @@ -542,8 +542,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= -gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss= +gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=