Skip to content

Commit 6c9752a

Browse files
authored
feat(iaas): volume encryption (#1103)
relates to STACKITTPR-413 and #1045
1 parent 8a429ea commit 6c9752a

File tree

8 files changed

+950
-46
lines changed

8 files changed

+950
-46
lines changed

docs/data-sources/volume.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ data "stackit_volume" "example" {
3535

3636
- `availability_zone` (String) The availability zone of the volume.
3737
- `description` (String) The description of the volume.
38+
- `encrypted` (Boolean) Indicates if the volume is encrypted.
3839
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`volume_id`".
3940
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
4041
- `name` (String) The name of the volume.

docs/resources/volume.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@ page_title: "stackit_volume Resource - stackit"
44
subcategory: ""
55
description: |-
66
Volume resource schema. Must have a region specified in the provider configuration.
7+
-> Note: Write-Only argument key_payload_base64_wo is available to use in place of key_payload_base64. Write-Only arguments are supported in HashiCorp Terraform 1.11.0 and later. Learn more https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments.
78
---
89

910
# stackit_volume (Resource)
1011

11-
Volume resource schema. Must have a `region` specified in the provider configuration.
12+
Volume resource schema. Must have a `region` specified in the provider configuration.
13+
14+
-> **Note:** Write-Only argument `key_payload_base64_wo` is available to use in place of `key_payload_base64`. Write-Only arguments are supported in HashiCorp Terraform 1.11.0 and later. [Learn more](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments).
1215

1316
## Example Usage
1417

@@ -41,6 +44,7 @@ import {
4144
### Optional
4245

4346
- `description` (String) The description of the volume.
47+
- `encryption_parameters` (Attributes) Parameter to connect to a key-encryption-key within the STACKIT-KMS to create encrypted volumes. These parameters never leave the backend again. So these parameters are not present on imports or in the datasource. They live only in your Terraform state after creation of the resource. (see [below for nested schema](#nestedatt--encryption_parameters))
4448
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
4549
- `name` (String) The name of the volume.
4650
- `performance_class` (String) The performance class of the volume. Possible values are documented in [Service plans BlockStorage](https://docs.stackit.cloud/products/storage/block-storage/basics/service-plans/#currently-available-service-plans-performance-classes)
@@ -50,10 +54,28 @@ import {
5054

5155
### Read-Only
5256

57+
- `encrypted` (Boolean) Indicates if the volume is encrypted.
5358
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`volume_id`".
5459
- `server_id` (String) The server ID of the server to which the volume is attached to.
5560
- `volume_id` (String) The volume ID.
5661

62+
<a id="nestedatt--encryption_parameters"></a>
63+
### Nested Schema for `encryption_parameters`
64+
65+
Required:
66+
67+
- `kek_key_id` (String) UUID of the key within the STACKIT-KMS to use for the encryption.
68+
- `kek_key_version` (Number) Version of the key within the STACKIT-KMS to use for the encryption.
69+
- `kek_keyring_id` (String) UUID of the keyring where the key is located within the STACKTI-KMS.
70+
- `service_account` (String) Service-Account linked to the Key within the STACKIT-KMS.
71+
72+
Optional:
73+
74+
- `key_payload_base64` (String, Sensitive) Optional predefined secret, which will be encrypted against the key-encryption-key within the STACKIT-KMS. If not defined, a random secret will be generated by the API and encrypted against the STACKIT-KMS. If a key-payload is provided here, it must be base64 encoded.
75+
- `key_payload_base64_wo` (String, Sensitive, [Write-only](https://developer.hashicorp.com/terraform/language/resources/ephemeral#write-only-arguments)) Optional predefined secret, which will be encrypted against the key-encryption-key within the STACKIT-KMS. If not defined, a random secret will be generated by the API and encrypted against the STACKIT-KMS. If a key-payload is provided here, it must be base64 encoded.
76+
- `key_payload_base64_wo_version` (Number) Used together with `key_payload_base64_wo` to trigger an re-create. Increment this value when an update to `key_payload_base64_wo` is required.
77+
78+
5779
<a id="nestedatt--source"></a>
5880
### Nested Schema for `source`
5981

stackit/internal/services/iaas/iaas_acc_test.go

Lines changed: 326 additions & 7 deletions
Large diffs are not rendered by default.

stackit/internal/services/iaas/testdata/resource-volume-max.tf

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ variable "size" {}
55
variable "description" {}
66
variable "performance_class" {}
77
variable "label" {}
8+
variable "key_payload_base64" {}
9+
variable "service_account_mail" {}
810

911
resource "stackit_volume" "volume_size" {
1012
project_id = var.project_id
@@ -33,4 +35,82 @@ resource "stackit_volume" "volume_source" {
3335
labels = {
3436
"acc-test" : var.label
3537
}
36-
}
38+
}
39+
40+
# just needed for the test setup for encrypted volumes
41+
resource "stackit_kms_keyring" "keyring" {
42+
project_id = var.project_id
43+
display_name = var.name
44+
}
45+
46+
# just needed for the test setup for encrypted volumes
47+
resource "stackit_kms_key" "key" {
48+
project_id = var.project_id
49+
keyring_id = stackit_kms_keyring.keyring.keyring_id
50+
display_name = var.name
51+
protection = "software"
52+
algorithm = "aes_256_gcm"
53+
purpose = "symmetric_encrypt_decrypt"
54+
}
55+
56+
resource "stackit_volume" "volume_encrypted_no_key_payload" {
57+
project_id = var.project_id
58+
availability_zone = var.availability_zone
59+
name = var.name
60+
size = var.size
61+
description = var.description
62+
performance_class = var.performance_class
63+
labels = {
64+
"acc-test" : var.label
65+
}
66+
67+
encryption_parameters = {
68+
kek_key_id = stackit_kms_key.key.key_id
69+
kek_key_version = 1
70+
kek_keyring_id = stackit_kms_keyring.keyring.keyring_id
71+
service_account = var.service_account_mail
72+
}
73+
}
74+
75+
# use the regular "key_payload_base64" field
76+
resource "stackit_volume" "volume_encrypted_with_regular_key_payload" {
77+
project_id = var.project_id
78+
availability_zone = var.availability_zone
79+
name = var.name
80+
size = var.size
81+
description = var.description
82+
performance_class = var.performance_class
83+
labels = {
84+
"acc-test" : var.label
85+
}
86+
87+
encryption_parameters = {
88+
kek_key_id = stackit_kms_key.key.key_id
89+
kek_key_version = 1
90+
kek_keyring_id = stackit_kms_keyring.keyring.keyring_id
91+
key_payload_base64 = var.key_payload_base64
92+
service_account = var.service_account_mail
93+
}
94+
}
95+
96+
# use the write-only "key_payload_base64_wo" field instead
97+
resource "stackit_volume" "volume_encrypted_with_write_only_key_payload" {
98+
project_id = var.project_id
99+
availability_zone = var.availability_zone
100+
name = var.name
101+
size = var.size
102+
description = var.description
103+
performance_class = var.performance_class
104+
labels = {
105+
"acc-test" : var.label
106+
}
107+
108+
encryption_parameters = {
109+
kek_key_id = stackit_kms_key.key.key_id
110+
kek_key_version = 1
111+
kek_keyring_id = stackit_kms_keyring.keyring.keyring_id
112+
key_payload_base64_wo = var.key_payload_base64
113+
key_payload_base64_wo_version = 1
114+
service_account = var.service_account_mail
115+
}
116+
}

stackit/internal/services/iaas/volume/datasource.go

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import (
55
"fmt"
66
"net/http"
77

8+
"github.com/hashicorp/terraform-plugin-framework/attr"
9+
"github.com/hashicorp/terraform-plugin-framework/diag"
10+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
11+
812
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
913
iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils"
1014

@@ -24,6 +28,23 @@ var (
2428
_ datasource.DataSource = &volumeDataSource{}
2529
)
2630

31+
type DatasourceModel struct {
32+
// basically the same as the resource model, just without encryption parameters as they are only **sent** to the API, but **never returned**
33+
Id types.String `tfsdk:"id"` // needed by TF
34+
ProjectId types.String `tfsdk:"project_id"`
35+
Region types.String `tfsdk:"region"`
36+
VolumeId types.String `tfsdk:"volume_id"`
37+
Name types.String `tfsdk:"name"`
38+
AvailabilityZone types.String `tfsdk:"availability_zone"`
39+
Labels types.Map `tfsdk:"labels"`
40+
Description types.String `tfsdk:"description"`
41+
PerformanceClass types.String `tfsdk:"performance_class"`
42+
Size types.Int64 `tfsdk:"size"`
43+
ServerId types.String `tfsdk:"server_id"`
44+
Source types.Object `tfsdk:"source"`
45+
Encrypted types.Bool `tfsdk:"encrypted"`
46+
}
47+
2748
// NewVolumeDataSource is a helper function to simplify the provider implementation.
2849
func NewVolumeDataSource() datasource.DataSource {
2950
return &volumeDataSource{}
@@ -134,13 +155,17 @@ func (d *volumeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest,
134155
},
135156
},
136157
},
158+
"encrypted": schema.BoolAttribute{
159+
Description: "Indicates if the volume is encrypted.",
160+
Computed: true,
161+
},
137162
},
138163
}
139164
}
140165

141166
// Read refreshes the Terraform state with the latest data.
142167
func (d *volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
143-
var model Model
168+
var model DatasourceModel
144169
diags := req.Config.Get(ctx, &model)
145170
resp.Diagnostics.Append(diags...)
146171
if resp.Diagnostics.HasError() {
@@ -174,7 +199,7 @@ func (d *volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest,
174199

175200
ctx = core.LogResponse(ctx)
176201

177-
err = mapFields(ctx, volumeResp, &model, region)
202+
err = mapDatasourceFields(ctx, volumeResp, &model, region)
178203
if err != nil {
179204
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", fmt.Sprintf("Processing API payload: %v", err))
180205
return
@@ -186,3 +211,62 @@ func (d *volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest,
186211
}
187212
tflog.Info(ctx, "volume read")
188213
}
214+
215+
func mapDatasourceFields(ctx context.Context, volumeResp *iaas.Volume, model *DatasourceModel, region string) error {
216+
if volumeResp == nil {
217+
return fmt.Errorf("response input is nil")
218+
}
219+
if model == nil {
220+
return fmt.Errorf("model input is nil")
221+
}
222+
223+
var volumeId string
224+
if model.VolumeId.ValueString() != "" {
225+
volumeId = model.VolumeId.ValueString()
226+
} else if volumeResp.Id != nil {
227+
volumeId = *volumeResp.Id
228+
} else {
229+
return fmt.Errorf("Volume id not present")
230+
}
231+
232+
model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, volumeId)
233+
model.Region = types.StringValue(region)
234+
235+
labels, err := iaasUtils.MapLabels(ctx, volumeResp.Labels, model.Labels)
236+
if err != nil {
237+
return err
238+
}
239+
240+
var sourceValues map[string]attr.Value
241+
var sourceObject basetypes.ObjectValue
242+
if volumeResp.Source == nil {
243+
sourceObject = types.ObjectNull(sourceTypes)
244+
} else {
245+
sourceValues = map[string]attr.Value{
246+
"type": types.StringPointerValue(volumeResp.Source.Type),
247+
"id": types.StringPointerValue(volumeResp.Source.Id),
248+
}
249+
var diags diag.Diagnostics
250+
sourceObject, diags = types.ObjectValue(sourceTypes, sourceValues)
251+
if diags.HasError() {
252+
return fmt.Errorf("creating source: %w", core.DiagsToError(diags))
253+
}
254+
}
255+
256+
model.VolumeId = types.StringValue(volumeId)
257+
model.AvailabilityZone = types.StringPointerValue(volumeResp.AvailabilityZone)
258+
model.Description = types.StringPointerValue(volumeResp.Description)
259+
model.Name = types.StringPointerValue(volumeResp.Name)
260+
// Workaround for volumes with no names which return an empty string instead of nil
261+
if name := volumeResp.Name; name != nil && *name == "" {
262+
model.Name = types.StringNull()
263+
}
264+
model.Labels = labels
265+
model.PerformanceClass = types.StringPointerValue(volumeResp.PerformanceClass)
266+
model.ServerId = types.StringPointerValue(volumeResp.ServerId)
267+
model.Size = types.Int64PointerValue(volumeResp.Size)
268+
model.Source = sourceObject
269+
model.Encrypted = types.BoolPointerValue(volumeResp.Encrypted)
270+
271+
return nil
272+
}

0 commit comments

Comments
 (0)