diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 8730608..448a613 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -14,6 +14,6 @@ jobs: - name: 'Checkout repository' uses: actions/checkout@v6 - name: 'Dependency Review' - uses: actions/dependency-review-action@v5 + uses: actions/dependency-review-action@v4 with: comment-summary-in-pr: on-failure diff --git a/.web-docs/components/builder/linode/README.md b/.web-docs/components/builder/linode/README.md index 4764f0c..ffcbbe4 100644 --- a/.web-docs/components/builder/linode/README.md +++ b/.web-docs/components/builder/linode/README.md @@ -90,20 +90,12 @@ 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 an image, at least one of root_pass, - authorized_keys, or authorized_users must be provided + you create a new Linode instance with a private image, you will be required to setup a + new root password. - `image_label` (string) - The name of the resulting image that will appear in your account. Defaults to `packer-{{timestamp}}` (see [configuration @@ -503,15 +495,11 @@ 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, - at least one of root_pass, authorized_keys, or authorized_users - must be provided to ensure access. +- `image` (string) - An Image ID to deploy the Linode Disk from. If provided, root_pass is required. - `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. diff --git a/builder/linode/builder_test.go b/builder/linode/builder_test.go index 4eb66e9..b88dfdd 100644 --- a/builder/linode/builder_test.go +++ b/builder/linode/builder_test.go @@ -13,12 +13,11 @@ 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", - "authorized_keys": []string{"ssh-rsa AAAA..."}, + "linode_token": "bar", + "region": "us-ord", + "instance_type": "g6-nanode-1", + "ssh_username": "root", + "image": "linode/arch", } } @@ -374,11 +373,9 @@ func TestBuilderPrepare_AuthorizedKeysAndUsers(t *testing.T) { var b Builder config := testConfig() - // 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 + // Test optional delete(config, "authorized_keys") delete(config, "authorized_users") - config["root_pass"] = "testpassword123" _, warnings, err := b.Prepare(config) if len(warnings) > 0 { @@ -417,293 +414,6 @@ 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_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 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) - } - }) - - 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("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", - "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 ssh_private_key_file is set but boot disk has no auth method") - } - errStr := err.Error() - if !strings.Contains(errStr, "root_pass, authorized_keys, or authorized_users") { - t.Fatalf("expected specific error message, got: %s", errStr) - } - }) - - 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) - } - }) - - 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) { var b Builder config := testConfig() @@ -1133,136 +843,6 @@ 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() @@ -1270,11 +850,10 @@ func TestBuilderPrepare_CustomDisks(t *testing.T) { // Test with custom disk config["disk"] = []map[string]any{ { - "label": "boot", - "size": 25000, - "image": "linode/arch", - "filesystem": "ext4", - "authorized_keys": []string{"ssh-rsa AAAA..."}, + "label": "boot", + "size": 25000, + "image": "linode/arch", + "filesystem": "ext4", }, { "label": "swap", @@ -1301,9 +880,7 @@ 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 { @@ -1358,16 +935,14 @@ func TestBuilderPrepare_CustomConfig(t *testing.T) { config["disk"] = []map[string]any{ { - "label": "boot", - "size": 25000, - "image": "linode/arch", - "authorized_keys": []string{"ssh-rsa AAAA..."}, + "label": "boot", + "size": 25000, + "image": "linode/arch", }, } - // When using custom disks, image and authorized_keys should not be at top level + // When using custom disks, image should not be required at top level delete(config, "image") - delete(config, "authorized_keys") _, warnings, err := b.Prepare(config) if len(warnings) > 0 { @@ -1618,13 +1193,12 @@ 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", "authorized_keys": []string{"ssh-rsa AAAA..."}}, + {"label": "boot", "size": 25000, "image": "linode/arch"}, } config["config"] = []map[string]any{ {"label": "my-config", "root_device": "/dev/sda", "devices": map[string]any{"sda": map[string]any{"disk_label": "boot"}}}, @@ -1640,9 +1214,8 @@ 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", "authorized_keys": []string{"ssh-rsa AAAA..."}}, + {"label": "boot", "size": 25000, "image": "linode/arch"}, } 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 66d0ea1..fc0d90e 100644 --- a/builder/linode/config.go +++ b/builder/linode/config.go @@ -4,6 +4,8 @@ package linode import ( + "crypto/rand" + "encoding/base64" "errors" "fmt" "os" @@ -42,18 +44,13 @@ 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, - // at least one of root_pass, authorized_keys, or authorized_users - // must be provided to ensure access. + // An Image ID to deploy the Linode Disk from. If provided, root_pass is required. 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"` @@ -293,23 +290,13 @@ 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 an image, at least one of root_pass, - // authorized_keys, or authorized_users must be provided + // you create a new Linode instance with a private image, you will be required to setup a + // new root password. RootPass string `mapstructure:"root_pass" required:"false"` // The name of the resulting image that will appear @@ -573,6 +560,16 @@ 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, @@ -616,6 +613,14 @@ 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 @@ -626,94 +631,11 @@ 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...) } - // 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. - // 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 && !willAutoGenerateKey { - errs = packersdk.MultiErrorAppend( - errs, - fmt.Errorf( - "when image is specified, at least one of root_pass, authorized_keys, or authorized_users must be provided", - ), - ) - } - } - - // 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 - - 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, - ), - ) - } - } - - // 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 - } - - if strings.TrimSpace(d.Image) == "" { - break - } - - 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 && !willAutoGenerateKey { - errs = packersdk.MultiErrorAppend( - errs, - fmt.Errorf( - "boot disk %q must define root_pass, authorized_keys, or authorized_users", - d.Label, - ), - ) - } - - break - } - } + c.Comm.SSHPassword = c.RootPass if c.PersonalAccessToken == "" { // Required configurations that will display errors if not set @@ -803,16 +725,6 @@ 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 fa53476..0db49e0 100644 --- a/builder/linode/config.hcl2spec.go +++ b/builder/linode/config.hcl2spec.go @@ -79,8 +79,6 @@ 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"` @@ -180,8 +178,6 @@ 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}, @@ -209,7 +205,6 @@ 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"` @@ -232,7 +227,6 @@ 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 62c7066..2fc6063 100644 --- a/builder/linode/config_test.go +++ b/builder/linode/config_test.go @@ -22,11 +22,10 @@ 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"}, - AuthorizedKeys: []string{"ssh-rsa AAAA..."}, + Region: "us-ord", + InstanceType: "g6-standard-1", + Image: "linode/debian12", + ImageRegions: []string{"us-ord", "us-mia", "us-lax"}, } warnings, err := config.Prepare() diff --git a/builder/linode/step_create_disk_config.go b/builder/linode/step_create_disk_config.go index 8b88f5c..a2b8162 100644 --- a/builder/linode/step_create_disk_config.go +++ b/builder/linode/step_create_disk_config.go @@ -24,7 +24,6 @@ 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, @@ -289,8 +288,9 @@ func (s *stepCreateDiskConfig) Run(ctx context.Context, state multistep.StateBag } } - // 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 + if diskOpts.RootPass == "" && diskOpts.Image != "" { + diskOpts.RootPass = c.Comm.Password() + } 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 e7202d8..888c58d 100644 --- a/builder/linode/step_create_linode.go +++ b/builder/linode/step_create_linode.go @@ -131,9 +131,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 { opts.DefaultRoute = &linodego.InterfaceDefaultRoute{ @@ -187,19 +185,12 @@ func (s *stepCreateLinode) Run(ctx context.Context, state multistep.StateBag) mu // Only set image-related options when NOT using custom disks if !useCustomDisks { - // 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.RootPass = c.Comm.Password() 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/go.mod b/go.mod index 9f04b5c..8b76716 100644 --- a/go.mod +++ b/go.mod @@ -7,10 +7,10 @@ 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.69.0 + github.com/linode/linodego v1.68.0 github.com/mitchellh/mapstructure v1.5.0 github.com/zclconf/go-cty v1.16.3 - golang.org/x/crypto v0.51.0 + golang.org/x/crypto v0.50.0 golang.org/x/oauth2 v0.36.0 ) @@ -133,21 +133,21 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect golang.org/x/exp v0.0.0-20230321023759-10a507213a29 // indirect - golang.org/x/mod v0.35.0 // indirect + golang.org/x/mod v0.34.0 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.44.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect + golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/tools v0.43.0 // indirect google.golang.org/api v0.272.0 // indirect google.golang.org/genproto v0.0.0-20260217215200-42d3e9bedb6d // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260217215200-42d3e9bedb6d // indirect 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.2 // indirect + gopkg.in/ini.v1 v1.67.1 // indirect ) // Temporary replacement for issue causing releases to fail (see: #379) diff --git a/go.sum b/go.sum index 3b9e577..50e9a85 100644 --- a/go.sum +++ b/go.sum @@ -287,8 +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/linode/linodego v1.68.0 h1:lAsXuHm/cwQT3KCbVpMGtRiH8IpQl4hUuBOXpqkuNwo= +github.com/linode/linodego v1.68.0/go.mod h1:X7nmTNq1GmZT4bG6w9aiuVrOnhVxYaywrzxM+buC/qU= 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= @@ -440,13 +440,13 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3 golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= -golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug= golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -498,29 +498,29 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= @@ -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.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss= -gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +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/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=