Skip to content

Commit 7d1073c

Browse files
akalcosmintyakin-akamaizliang-akamai
authored
Changed volume encryption to enabled by default. (#2139)
* Changed volume encryption to enabled by default. * Tests for volume encryption setting * go fmt fixes * tests(volume): fix comment to reference DataWithBlockStorageEncryptionDisabled * When encryption is omitted, New volumes in regions supporting “Block Storage Encryption” are created with encryption="enabled". New volumes in other regions follow the API default. Existing volumes are unaffected unless the user explicitly changes encryption * volume: default encryption to "enabled" on create; show in plan - Default encryption to "enabled" when omitted on create; remove region capability checks - Add DefaultEnabledOnCreate plan modifier so plans show encryption="enabled" for new resources - Keep Optional+Computed + UseStateForUnknown so omission on existing resources preserves state (no diff) - Keep RequiresReplace so explicit encryption changes force replacement - Unit: assert presence of DefaultEnabledOnCreate in volume schema * apply golangci-lint fmt (gci/gofumpt/goimports) and fix tests formatting * Add volume encryption check to smoke test and drop redundant case * Consolidate volume encryption acceptance tests into resource_test.go * tidy schema comments * drop DS additions * Fix data source test assertion * Rename test files --------- Co-authored-by: Tunc Yakin <tyakin@akamai.com> Co-authored-by: Zhiwei Liang <zliang@akamai.com> Co-authored-by: Zhiwei Liang <121905282+zliang-akamai@users.noreply.github.com>
1 parent fb07c72 commit 7d1073c

8 files changed

+239
-7
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package volume
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
7+
"github.com/hashicorp/terraform-plugin-framework/types"
8+
)
9+
10+
// defaultEnabledOnCreate is a String plan modifier that sets the planned value to
11+
// "enabled" during create when the attribute is omitted (null/unknown).
12+
// It does nothing on updates (when a prior state value exists).
13+
//
14+
// This keeps the plan consistent with Create() behavior without affecting updates,
15+
// where UseStateForUnknown preserves the existing state when the field is omitted.
16+
type defaultEnabledOnCreate struct{}
17+
18+
func DefaultEnabledOnCreate() planmodifier.String { return defaultEnabledOnCreate{} }
19+
20+
func (m defaultEnabledOnCreate) Description(ctx context.Context) string {
21+
return "Defaults to \"enabled\" on create when encryption is omitted"
22+
}
23+
24+
func (m defaultEnabledOnCreate) MarkdownDescription(ctx context.Context) string {
25+
return m.Description(ctx)
26+
}
27+
28+
func (m defaultEnabledOnCreate) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
29+
// If there is a prior state value, this is an update; do nothing.
30+
if !req.StateValue.IsNull() && !req.StateValue.IsUnknown() {
31+
return
32+
}
33+
// Create path: if the user omitted encryption (null/unknown), set planned value to "enabled".
34+
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
35+
resp.PlanValue = types.StringValue("enabled")
36+
}
37+
}

linode/volume/datasource_test.go renamed to linode/volume/framework_datasource_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestAccDataSourceVolume_basic(t *testing.T) {
3131
resource.TestCheckResourceAttr(resourceName, "linode_id", "0"),
3232
resource.TestCheckResourceAttrSet(resourceName, "created"),
3333
resource.TestCheckResourceAttrSet(resourceName, "updated"),
34-
resource.TestCheckResourceAttr(resourceName, "encryption", "disabled"),
34+
resource.TestCheckResourceAttr(resourceName, "encryption", "enabled"),
3535
),
3636
},
3737
},

linode/volume/framework_resource.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,17 @@ func (r *Resource) CreateVolume(
216216
size := helper.FrameworkSafeInt64ToInt(data.Size.ValueInt64(), diags)
217217

218218
createOpts := linodego.VolumeCreateOptions{
219-
Label: data.Label.ValueString(),
220-
Region: data.Region.ValueString(),
221-
Size: size,
222-
Encryption: data.Encryption.ValueString(),
219+
Label: data.Label.ValueString(),
220+
Region: data.Region.ValueString(),
221+
Size: size,
222+
}
223+
224+
// Respect explicit user setting first
225+
if !data.Encryption.IsNull() && !data.Encryption.IsUnknown() && data.Encryption.ValueString() != "" {
226+
createOpts.Encryption = data.Encryption.ValueString()
227+
} else {
228+
// Default to enabled on create when encryption is omitted, without region capability checks.
229+
createOpts.Encryption = "enabled"
223230
}
224231

225232
diags.Append(data.Tags.ElementsAs(ctx, &createOpts.Tags, false)...)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
1313
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
14+
"github.com/hashicorp/terraform-plugin-testing/terraform"
1415
"github.com/linode/linodego"
1516
"github.com/linode/terraform-provider-linode/v3/linode/acceptance"
1617
"github.com/linode/terraform-provider-linode/v3/linode/volume/tmpl"
@@ -88,6 +89,7 @@ func TestAccResourceVolume_basic_smoke(t *testing.T) {
8889
resource.TestCheckResourceAttrSet(resName, "size"),
8990
resource.TestCheckResourceAttr(resName, "label", volumeName),
9091
resource.TestCheckResourceAttr(resName, "region", testRegion),
92+
resource.TestCheckResourceAttr(resName, "encryption", "enabled"),
9193
resource.TestCheckResourceAttr(resName, "linode_id", "0"),
9294
resource.TestCheckResourceAttr(resName, "tags.#", "1"),
9395
resource.TestCheckResourceAttr(resName, "tags.0", "tf_test"),
@@ -380,6 +382,109 @@ func TestAccResourceVolume_cloned(t *testing.T) {
380382
})
381383
}
382384

385+
// Explicit encryption enabled (resource test)
386+
func TestAccResourceVolume_encryptionExplicitEnabled(t *testing.T) {
387+
t.Parallel()
388+
389+
volumeName := acctest.RandomWithPrefix("tf_test")
390+
resName := "linode_volume.foobar"
391+
392+
targetRegion, err := acceptance.GetRandomRegionWithCaps(nil, "core")
393+
if err != nil {
394+
t.Fatal(err)
395+
}
396+
397+
volume := linodego.Volume{}
398+
resource.Test(t, resource.TestCase{
399+
PreCheck: func() { acceptance.PreCheck(t) },
400+
ProtoV6ProviderFactories: acceptance.ProtoV6ProviderFactories,
401+
CheckDestroy: acceptance.CheckVolumeDestroy,
402+
Steps: []resource.TestStep{
403+
{
404+
Config: tmpl.DataWithBlockStorageEncryption(t, volumeName, targetRegion),
405+
Check: resource.ComposeTestCheckFunc(
406+
acceptance.CheckVolumeExists(resName, &volume),
407+
resource.TestCheckResourceAttr(resName, "encryption", "enabled"),
408+
),
409+
},
410+
},
411+
})
412+
}
413+
414+
// Explicit encryption disabled (resource test)
415+
func TestAccResourceVolume_encryptionExplicitDisabled(t *testing.T) {
416+
t.Parallel()
417+
418+
volumeName := acctest.RandomWithPrefix("tf_test")
419+
resName := "linode_volume.foobar"
420+
421+
targetRegion, err := acceptance.GetRandomRegionWithCaps(nil, "core")
422+
if err != nil {
423+
t.Fatal(err)
424+
}
425+
426+
volume := linodego.Volume{}
427+
resource.Test(t, resource.TestCase{
428+
PreCheck: func() { acceptance.PreCheck(t) },
429+
ProtoV6ProviderFactories: acceptance.ProtoV6ProviderFactories,
430+
CheckDestroy: acceptance.CheckVolumeDestroy,
431+
Steps: []resource.TestStep{
432+
{
433+
Config: tmpl.DataWithBlockStorageEncryptionDisabled(t, volumeName, targetRegion),
434+
Check: resource.ComposeTestCheckFunc(
435+
acceptance.CheckVolumeExists(resName, &volume),
436+
resource.TestCheckResourceAttr(resName, "encryption", "disabled"),
437+
),
438+
},
439+
},
440+
})
441+
}
442+
443+
// Changing encryption forces replacement (verify ID changes)
444+
func TestAccResourceVolume_encryptionChangeForcesReplace(t *testing.T) {
445+
t.Parallel()
446+
447+
volumeName := acctest.RandomWithPrefix("tf_test")
448+
resName := "linode_volume.foobar"
449+
450+
targetRegion, err := acceptance.GetRandomRegionWithCaps(nil, "core")
451+
if err != nil {
452+
t.Fatal(err)
453+
}
454+
455+
var v linodego.Volume
456+
var firstID int
457+
458+
resource.Test(t, resource.TestCase{
459+
PreCheck: func() { acceptance.PreCheck(t) },
460+
ProtoV6ProviderFactories: acceptance.ProtoV6ProviderFactories,
461+
CheckDestroy: acceptance.CheckVolumeDestroy,
462+
Steps: []resource.TestStep{
463+
{
464+
Config: tmpl.DataWithBlockStorageEncryptionDisabled(t, volumeName, targetRegion),
465+
Check: resource.ComposeTestCheckFunc(
466+
acceptance.CheckVolumeExists(resName, &v),
467+
resource.TestCheckResourceAttr(resName, "encryption", "disabled"),
468+
func(_ *terraform.State) error { firstID = v.ID; return nil },
469+
),
470+
},
471+
{
472+
Config: tmpl.DataWithBlockStorageEncryption(t, volumeName, targetRegion),
473+
Check: resource.ComposeTestCheckFunc(
474+
acceptance.CheckVolumeExists(resName, &v),
475+
resource.TestCheckResourceAttr(resName, "encryption", "enabled"),
476+
func(_ *terraform.State) error {
477+
if v.ID == firstID {
478+
return fmt.Errorf("expected replacement, ID unchanged: %d", v.ID)
479+
}
480+
return nil
481+
},
482+
),
483+
},
484+
},
485+
})
486+
}
487+
383488
const scriptFormatDrive = `
384489
until [ -e "%s" ]; do sleep .1; done && \
385490
mkfs.ext4 "%s" && \

linode/volume/framework_schema_resource.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
1111
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
1212
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
13-
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
1413
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
1514
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
1615
"github.com/hashicorp/terraform-plugin-framework/types"
@@ -116,11 +115,12 @@ var frameworkResourceSchema = schema.Schema{
116115
Description: "Whether Block Storage Disk Encryption is enabled or disabled on this Volume. ",
117116
Optional: true,
118117
Computed: true,
119-
Default: stringdefault.StaticString("disabled"),
120118
Validators: []validator.String{
121119
stringvalidator.OneOf("enabled", "disabled"),
122120
},
123121
PlanModifiers: []planmodifier.String{
122+
stringplanmodifier.UseStateForUnknown(),
123+
DefaultEnabledOnCreate(),
124124
stringplanmodifier.RequiresReplace(),
125125
},
126126
},
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//go:build unit
2+
3+
package volume
4+
5+
import (
6+
"reflect"
7+
"testing"
8+
9+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
10+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
// test that guards the schema for the encryption attribute.
15+
func TestEncryptionAttribute_HasDefaultAndRequiresReplace(t *testing.T) {
16+
t.Helper()
17+
18+
attrRaw, ok := frameworkResourceSchema.Attributes["encryption"]
19+
require.True(t, ok, "encryption attribute must exist in schema")
20+
21+
attr, ok := attrRaw.(schema.StringAttribute)
22+
require.True(t, ok, "encryption must be a StringAttribute")
23+
24+
// Should be Optional + Computed with no schema default; provider preserves state when omitted.
25+
require.True(t, attr.Optional, "encryption should be Optional")
26+
require.True(t, attr.Computed, "encryption should be Computed")
27+
require.Nil(t, attr.Default, "encryption should not have a schema default")
28+
29+
// Must preserve state when config omits the field.
30+
expectedUseStateType := reflect.TypeOf(stringplanmodifier.UseStateForUnknown())
31+
foundUseState := false
32+
for _, pm := range attr.PlanModifiers {
33+
if reflect.TypeOf(pm) == expectedUseStateType {
34+
foundUseState = true
35+
break
36+
}
37+
}
38+
require.True(t, foundUseState, "encryption should have UseStateForUnknown plan modifier")
39+
40+
// Must require replacement when changed.
41+
expectedReplaceType := reflect.TypeOf(stringplanmodifier.RequiresReplace())
42+
foundReplace := false
43+
for _, pm := range attr.PlanModifiers {
44+
if reflect.TypeOf(pm) == expectedReplaceType {
45+
foundReplace = true
46+
break
47+
}
48+
}
49+
require.True(t, foundReplace, "encryption should have a RequiresReplace plan modifier")
50+
51+
// Should show default "enabled" on create when omitted.
52+
expectedDefaultOnCreateType := reflect.TypeOf(DefaultEnabledOnCreate())
53+
foundDefaultOnCreate := false
54+
for _, pm := range attr.PlanModifiers {
55+
if reflect.TypeOf(pm) == expectedDefaultOnCreateType {
56+
foundDefaultOnCreate = true
57+
break
58+
}
59+
}
60+
require.True(t, foundDefaultOnCreate, "encryption should have DefaultEnabledOnCreate plan modifier")
61+
62+
// Should have validators (e.g., OneOf("enabled","disabled")). We don't assert exact type, just presence.
63+
require.NotEmpty(t, attr.Validators, "encryption should have validators (e.g., OneOf)")
64+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{{ define "volume_data_with_block_storage_encryption_disabled" }}
2+
3+
resource "linode_volume" "foobar" {
4+
label = "{{.Label}}"
5+
region = "{{ .Region }}"
6+
tags = ["tf_test"]
7+
encryption = "disabled"
8+
}
9+
10+
data "linode_volume" "foobar" {
11+
id = "${linode_volume.foobar.id}"
12+
}
13+
14+
{{ end }}

linode/volume/tmpl/template.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,8 @@ func DataWithBlockStorageEncryption(t *testing.T, volume, region string) string
6969
return acceptance.ExecuteTemplate(t,
7070
"volume_data_with_block_storage_encryption", TemplateData{Label: volume, Region: region})
7171
}
72+
73+
func DataWithBlockStorageEncryptionDisabled(t *testing.T, volume, region string) string {
74+
return acceptance.ExecuteTemplate(t,
75+
"volume_data_with_block_storage_encryption_disabled", TemplateData{Label: volume, Region: region})
76+
}

0 commit comments

Comments
 (0)