diff --git a/.web-docs/components/post-processor/upcloud-import/README.md b/.web-docs/components/post-processor/upcloud-import/README.md index 8e31a1d..26ecaa7 100644 --- a/.web-docs/components/post-processor/upcloud-import/README.md +++ b/.web-docs/components/post-processor/upcloud-import/README.md @@ -31,6 +31,10 @@ Username and password configuration arguments can be omitted if environment vari - `storage_tier` (string) - The storage tier to use. Available options are `maxiops`, `archive`, and `standard`. Defaults to `maxiops`. +- `storage_size` (int) - The storage size in gigabytes. If not specified, defaults to the image size + (minimum 10GB). When importing compressed images that expand significantly, specify + a larger value to ensure adequate space for the uncompressed content. + - `state_timeout_duration` (duration string | ex: "1h5m2s") - The amount of time to wait for resource state changes. Defaults to `60m`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aa6058..4e57761 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/) ## [Unreleased] +### Added + +- `storage_size` parameter to upcloud-import post-processor + ### Fixed - Update Go version to 1.24.6 diff --git a/docs-partials/post-processor/upcloud-import/Config-not-required.mdx b/docs-partials/post-processor/upcloud-import/Config-not-required.mdx index 05c5f8b..820eb13 100644 --- a/docs-partials/post-processor/upcloud-import/Config-not-required.mdx +++ b/docs-partials/post-processor/upcloud-import/Config-not-required.mdx @@ -10,6 +10,10 @@ - `storage_tier` (string) - The storage tier to use. Available options are `maxiops`, `archive`, and `standard`. Defaults to `maxiops`. +- `storage_size` (int) - The storage size in gigabytes. If not specified, defaults to the image size + (minimum 10GB). When importing compressed images that expand significantly, specify + a larger value to ensure adequate space for the uncompressed content. + - `state_timeout_duration` (duration string | ex: "1h5m2s") - The amount of time to wait for resource state changes. Defaults to `60m`. diff --git a/post-processor/upcloud-import/config.go b/post-processor/upcloud-import/config.go index 3f6a5b1..8dee1b5 100644 --- a/post-processor/upcloud-import/config.go +++ b/post-processor/upcloud-import/config.go @@ -40,6 +40,11 @@ type Config struct { // The storage tier to use. Available options are `maxiops`, `archive`, and `standard`. Defaults to `maxiops`. StorageTier string `mapstructure:"storage_tier"` + // The storage size in gigabytes. If not specified, defaults to the image size + // (minimum 10GB). When importing compressed images that expand significantly, specify + // a larger value to ensure adequate space for the uncompressed content. + StorageSize int `mapstructure:"storage_size"` + // The amount of time to wait for resource state changes. Defaults to `60m`. Timeout time.Duration `mapstructure:"state_timeout_duration"` @@ -92,6 +97,20 @@ func (c *Config) validate() *packer.MultiError { errs = packer.MultiErrorAppend(errs, authErrs.Errors...) } + // Validate storage size if specified + if c.StorageSize > 0 { + if c.StorageSize < storageMinSizeGB { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("'storage_size' must be at least %dGB", storageMinSizeGB), + ) + } + if c.StorageSize > storageMaxSizeGB { + errs = packer.MultiErrorAppend( + errs, fmt.Errorf("'storage_size' cannot exceed %dGB", storageMaxSizeGB), + ) + } + } + return errs } diff --git a/post-processor/upcloud-import/config.hcl2spec.go b/post-processor/upcloud-import/config.hcl2spec.go index 539c080..b670362 100644 --- a/post-processor/upcloud-import/config.hcl2spec.go +++ b/post-processor/upcloud-import/config.hcl2spec.go @@ -17,6 +17,7 @@ type FlatConfig struct { TemplateName *string `mapstructure:"template_name" required:"true" cty:"template_name" hcl:"template_name"` ReplaceExisting *bool `mapstructure:"replace_existing" cty:"replace_existing" hcl:"replace_existing"` StorageTier *string `mapstructure:"storage_tier" cty:"storage_tier" hcl:"storage_tier"` + StorageSize *int `mapstructure:"storage_size" cty:"storage_size" hcl:"storage_size"` Timeout *string `mapstructure:"state_timeout_duration" cty:"state_timeout_duration" hcl:"state_timeout_duration"` PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"` PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"` @@ -47,6 +48,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "template_name": &hcldec.AttrSpec{Name: "template_name", Type: cty.String, Required: false}, "replace_existing": &hcldec.AttrSpec{Name: "replace_existing", Type: cty.Bool, Required: false}, "storage_tier": &hcldec.AttrSpec{Name: "storage_tier", Type: cty.String, Required: false}, + "storage_size": &hcldec.AttrSpec{Name: "storage_size", Type: cty.Number, Required: false}, "state_timeout_duration": &hcldec.AttrSpec{Name: "state_timeout_duration", Type: cty.String, Required: false}, "packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false}, "packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false}, diff --git a/post-processor/upcloud-import/config_test.go b/post-processor/upcloud-import/config_test.go index d30c92a..e3ff3b1 100644 --- a/post-processor/upcloud-import/config_test.go +++ b/post-processor/upcloud-import/config_test.go @@ -286,3 +286,95 @@ func TestNewConfig_StorageTierDefault(t *testing.T) { require.NotNil(t, c) assert.Equal(t, "maxiops", c.StorageTier) } + +func TestNewConfig_StorageSize(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + storageSize interface{} // Use interface{} to test both presence and absence + expectError bool + expectedSize int + errorContains string + }{ + { + name: "not specified - should default to 0", + storageSize: nil, // Don't include storage_size in config + expectError: false, + expectedSize: 0, + }, + { + name: "valid size", + storageSize: 50, + expectError: false, + expectedSize: 50, + }, + { + name: "minimum valid size", + storageSize: 10, + expectError: false, + expectedSize: 10, + }, + { + name: "maximum valid size", + storageSize: 4096, + expectError: false, + expectedSize: 4096, + }, + { + name: "too small - below minimum", + storageSize: 5, + expectError: true, + errorContains: "'storage_size' must be at least 10GB", + }, + { + name: "too large - above maximum", + storageSize: 5000, + expectError: true, + errorContains: "'storage_size' cannot exceed 4096GB", + }, + { + name: "zero - invalid", + storageSize: 0, + expectError: false, // 0 means not specified, which is valid + expectedSize: 0, + }, + { + name: "negative - invalid", + storageSize: -10, + expectError: false, // Negative values are treated as 0 (not specified) + expectedSize: -10, // The value is stored as-is, validation only checks > 0 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + configMap := map[string]interface{}{ + "token": "test-token", + "zones": []string{"fi-hel1"}, + "template_name": "my-template", + } + + if tt.storageSize != nil { + configMap["storage_size"] = tt.storageSize + } + + c, err := upcloudimport.NewConfig([]interface{}{configMap}...) + + if tt.expectError { + assert.Error(t, err) + if tt.errorContains != "" { + assert.Contains(t, err.Error(), tt.errorContains) + } + } else { + assert.NoError(t, err) + require.NotNil(t, c) + assert.Equal(t, tt.expectedSize, c.StorageSize) + } + + require.NotNil(t, c) + }) + } +} diff --git a/post-processor/upcloud-import/step_create_storage.go b/post-processor/upcloud-import/step_create_storage.go index 5b93781..b2c2dbf 100644 --- a/post-processor/upcloud-import/step_create_storage.go +++ b/post-processor/upcloud-import/step_create_storage.go @@ -30,11 +30,17 @@ func (s *stepCreateStorage) Run(ctx context.Context, state multistep.StateBag) m if err != nil { return haltOnError(ui, state, err) } - size := s.image.SizeGB() - if size < storageMinSizeGB { - size = storageMinSizeGB + var size int + if s.postProcessor.config.StorageSize > 0 { + size = s.postProcessor.config.StorageSize + ui.Say(fmt.Sprintf("Creating storage device (%dGB) for '%s' image using manually specified size", size, s.image.File())) + } else { + size = s.image.SizeGB() + if size < storageMinSizeGB { + size = storageMinSizeGB + } + ui.Say(fmt.Sprintf("Creating storage device (%dGB) for '%s' image", size, s.image.File())) } - ui.Say(fmt.Sprintf("Creating storage device (%dGB) for '%s' image", size, s.image.File())) storage, err := s.postProcessor.driver.CreateTemplateStorage(ctx, fmt.Sprintf("%s-%s", BuilderID, time.Now().Format(timestampSuffixLayout)), s.postProcessor.config.Zones[0],