Skip to content

Commit e6b092c

Browse files
committed
feat(iaas): volume encryption
relates to STACKITTPR-413 and #1045
1 parent 459122c commit e6b092c

File tree

8 files changed

+756
-44
lines changed

8 files changed

+756
-44
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: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
### Optional
4242

4343
- `description` (String) The description of the volume.
44+
- `encryption_parameters` (Attributes) Parameter to connect to a key-encryption-key within the STACKIT-KMS to create encrypted volumes. (see [below for nested schema](#nestedatt--encryption_parameters))
4445
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
4546
- `name` (String) The name of the volume.
4647
- `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 +51,26 @@ import {
5051

5152
### Read-Only
5253

54+
- `encrypted` (Boolean) Indicates if the volume is encrypted.
5355
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`region`,`volume_id`".
5456
- `server_id` (String) The server ID of the server to which the volume is attached to.
5557
- `volume_id` (String) The volume ID.
5658

59+
<a id="nestedatt--encryption_parameters"></a>
60+
### Nested Schema for `encryption_parameters`
61+
62+
Required:
63+
64+
- `kek_key_id` (String) UUID of the key within the STACKIT-KMS to use for the encryption.
65+
- `kek_key_version` (Number) Version of the key within the STACKIT-KMS to use for the encryption.
66+
- `kek_keyring_id` (String) UUID of the keyring where the key is located within the STACKTI-KMS.
67+
- `service_account` (String) Service-Account linked to the Key within the STACKIT-KMS.
68+
69+
Optional:
70+
71+
- `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.
72+
73+
5774
<a id="nestedatt--source"></a>
5875
### Nested Schema for `source`
5976

stackit/internal/services/iaas/iaas_acc_test.go

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

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

Lines changed: 58 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,59 @@ 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+
resource "stackit_volume" "volume_encrypted_with_key_payload" {
76+
project_id = var.project_id
77+
availability_zone = var.availability_zone
78+
name = var.name
79+
size = var.size
80+
description = var.description
81+
performance_class = var.performance_class
82+
labels = {
83+
"acc-test" : var.label
84+
}
85+
86+
encryption_parameters = {
87+
kek_key_id = stackit_kms_key.key.key_id
88+
kek_key_version = 1
89+
kek_keyring_id = stackit_kms_keyring.keyring.keyring_id
90+
key_payload_base64 = var.key_payload_base64
91+
service_account = var.service_account_mail
92+
}
93+
}

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)