Skip to content

Commit 584f6fb

Browse files
oarbusilantoli
andauthored
feat: Support configuring BYOK encryption on search nodes (#3199)
* feat: Supports configuring BYOK encryption on search nodes (#3142) * use SDK preview in encryption_at_rest * changelog * Revert "use SDK preview in encryption_at_rest" This reverts commit 609c9dc. * trigger change in EAR * Revert "trigger change in EAR" This reverts commit 15794dd. * Reapply "use SDK preview in encryption_at_rest" This reverts commit 1c2db30. * TEMPORARY: send enabled_for_search_nodes = true * finish resource implementation and tests * data source implementation and test * doc update * default and refactor test * remove old migration test * default value in resource * unit test --------- Co-authored-by: Oriol Arbusi <[email protected]> * feat: Adds `encryption_at_rest_provider` to `mongodbatlas_search_deployment` resource and data source (#3152) * use preview * add encryption_at_rest_provider computed attribute * remove check * dosc * rename files * move adv_cluster config out of resources * fix config * project id * add TODO to version * doc: Updates examples with newly added attributes to `mongodbatlas_search_deployment` and `mongodbatlas_encryption_at_rest` (#3174) * add new attribute to the example * examples updates * nit: end with new line * fix tf validate * todos * PR suggestions + test failure --------- Co-authored-by: Leo Antoli <[email protected]>
1 parent b8db910 commit 584f6fb

29 files changed

+247
-216
lines changed

.changelog/3142.txt

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```release-note:enhancement
2+
resource/mongodbatlas_encryption_at_rest: Adds `enabled_for_search_nodes` attribute
3+
```
4+
5+
```release-note:enhancement
6+
data-source/mongodbatlas_encryption_at_rest: Adds `enabled_for_search_nodes` attribute
7+
```

.changelog/3152.txt

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
```release-note:enhancement
2+
resource/mongodbatlas_search_deployment: Adds `encryption_at_rest_provider` computed attribute
3+
```
4+
5+
```release-note:enhancement
6+
data-source/mongodbatlas_search_deployment: Adds `encryption_at_rest_provider` computed attribute
7+
```

docs/data-sources/encryption_at_rest.md

+3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ resource "mongodbatlas_encryption_at_rest" "test" {
4141
region = var.atlas_region
4242
role_id = mongodbatlas_cloud_provider_access_authorization.auth_role.role_id
4343
}
44+
45+
enabled_for_search_nodes = true
4446
}
4547
4648
resource "mongodbatlas_advanced_cluster" "cluster" {
@@ -135,6 +137,7 @@ output "is_gcp_encryption_at_rest_valid" {
135137

136138
- `aws_kms_config` (Attributes) Amazon Web Services (AWS) KMS configuration details and encryption at rest configuration set for the specified project. (see [below for nested schema](#nestedatt--aws_kms_config))
137139
- `azure_key_vault_config` (Attributes) Details that define the configuration of Encryption at Rest using Azure Key Vault (AKV). (see [below for nested schema](#nestedatt--azure_key_vault_config))
140+
- `enabled_for_search_nodes` (Boolean) Flag that indicates whether Encryption at Rest for Dedicated Search Nodes is enabled in the specified project.
138141
- `google_cloud_kms_config` (Attributes) Details that define the configuration of Encryption at Rest using Google Cloud Key Management Service (KMS). (see [below for nested schema](#nestedatt--google_cloud_kms_config))
139142
- `id` (String) The ID of this resource.
140143

docs/data-sources/search_deployment.md

+5
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ data "mongodbatlas_search_deployment" "example" {
4646
output "mongodbatlas_search_deployment_id" {
4747
value = data.mongodbatlas_search_deployment.example.id
4848
}
49+
50+
output "mongodbatlas_search_deployment_encryption_at_rest_provider" {
51+
value = data.mongodbatlas_search_deployment.example.encryption_at_rest_provider
52+
}
4953
```
5054

5155
<!-- schema generated by tfplugindocs -->
@@ -58,6 +62,7 @@ output "mongodbatlas_search_deployment_id" {
5862

5963
### Read-Only
6064

65+
- `encryption_at_rest_provider` (String) Cloud service provider that manages your customer keys to provide an additional layer of Encryption At Rest for the cluster.
6166
- `id` (String) Unique 24-hexadecimal digit string that identifies the search deployment.
6267
- `specs` (Attributes List) List of settings that configure the search nodes for your cluster. This list is currently limited to defining a single element. (see [below for nested schema](#nestedatt--specs))
6368
- `state_name` (String) Human-readable label that indicates the current operating condition of this search deployment.

docs/resources/encryption_at_rest.md

+3
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ resource "mongodbatlas_encryption_at_rest" "test" {
5656
region = var.atlas_region
5757
role_id = mongodbatlas_cloud_provider_access_authorization.auth_role.role_id
5858
}
59+
60+
enabled_for_search_nodes = true
5961
}
6062
6163
resource "mongodbatlas_advanced_cluster" "cluster" {
@@ -155,6 +157,7 @@ resource "mongodbatlas_encryption_at_rest" "test" {
155157

156158
- `aws_kms_config` (Block List) Amazon Web Services (AWS) KMS configuration details and encryption at rest configuration set for the specified project. (see [below for nested schema](#nestedblock--aws_kms_config))
157159
- `azure_key_vault_config` (Block List) Details that define the configuration of Encryption at Rest using Azure Key Vault (AKV). (see [below for nested schema](#nestedblock--azure_key_vault_config))
160+
- `enabled_for_search_nodes` (Boolean) Flag that indicates whether Encryption at Rest for Dedicated Search Nodes is enabled in the specified project.
158161
- `google_cloud_kms_config` (Block List) Details that define the configuration of Encryption at Rest using Google Cloud Key Management Service (KMS). (see [below for nested schema](#nestedblock--google_cloud_kms_config))
159162

160163
### Read-Only

docs/resources/search_deployment.md

+5
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ data "mongodbatlas_search_deployment" "example" {
5151
output "mongodbatlas_search_deployment_id" {
5252
value = data.mongodbatlas_search_deployment.example.id
5353
}
54+
55+
output "mongodbatlas_search_deployment_encryption_at_rest_provider" {
56+
value = data.mongodbatlas_search_deployment.example.encryption_at_rest_provider
57+
}
5458
```
5559

5660
<!-- schema generated by tfplugindocs -->
@@ -68,6 +72,7 @@ output "mongodbatlas_search_deployment_id" {
6872

6973
### Read-Only
7074

75+
- `encryption_at_rest_provider` (String) Cloud service provider that manages your customer keys to provide an additional layer of Encryption At Rest for the cluster.
7176
- `id` (String) Unique 24-hexadecimal digit string that identifies the search deployment.
7277
- `state_name` (String) Human-readable label that indicates the current operating condition of this search deployment.
7378

examples/mongodbatlas_encryption_at_rest/aws/atlas-cluster/main.tf

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ resource "mongodbatlas_encryption_at_rest" "test" {
2121
region = var.atlas_region
2222
role_id = mongodbatlas_cloud_provider_access_authorization.auth_role.role_id
2323
}
24+
25+
enabled_for_search_nodes = true
2426
}
2527

2628
resource "mongodbatlas_advanced_cluster" "cluster" {

examples/mongodbatlas_search_deployment/main.tf

+4
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,7 @@ data "mongodbatlas_search_deployment" "example" {
4040
output "mongodbatlas_search_deployment_id" {
4141
value = data.mongodbatlas_search_deployment.example.id
4242
}
43+
44+
output "mongodbatlas_search_deployment_encryption_at_rest_provider" {
45+
value = data.mongodbatlas_search_deployment.example.encryption_at_rest_provider
46+
}

internal/service/advancedcluster/resource_advanced_cluster_test.go

+2-29
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func testAccAdvancedClusterFlexUpgrade(t *testing.T, instanceSize string, includ
136136
}
137137
if includeDedicated {
138138
steps = append(steps, resource.TestStep{
139-
Config: acc.ConvertAdvancedClusterToPreviewProviderV2(t, true, configBasicDedicated(projectID, clusterName, defaultZoneName)),
139+
Config: acc.ConvertAdvancedClusterToPreviewProviderV2(t, true, acc.ConfigBasicDedicated(projectID, clusterName, defaultZoneName)),
140140
Check: checksBasicDedicated(projectID, clusterName),
141141
})
142142
}
@@ -171,7 +171,7 @@ func TestAccMockableAdvancedCluster_tenantUpgrade(t *testing.T) {
171171
Check: checkTenant(true, projectID, clusterName),
172172
},
173173
{
174-
Config: acc.ConvertAdvancedClusterToPreviewProviderV2(t, true, configBasicDedicated(projectID, clusterName, defaultZoneName)),
174+
Config: acc.ConvertAdvancedClusterToPreviewProviderV2(t, true, acc.ConfigBasicDedicated(projectID, clusterName, defaultZoneName)),
175175
Check: checksBasicDedicated(projectID, clusterName),
176176
},
177177
acc.TestStepImportCluster(resourceName),
@@ -1659,33 +1659,6 @@ func checkTenant(usePreviewProvider bool, projectID, name string) resource.TestC
16591659
pluralChecks...)
16601660
}
16611661

1662-
func configBasicDedicated(projectID, name, zoneName string) string {
1663-
zoneNameLine := ""
1664-
if zoneName != "" {
1665-
zoneNameLine = fmt.Sprintf("zone_name = %q", zoneName)
1666-
}
1667-
return fmt.Sprintf(`
1668-
resource "mongodbatlas_advanced_cluster" "test" {
1669-
project_id = %[1]q
1670-
name = %[2]q
1671-
cluster_type = "REPLICASET"
1672-
1673-
replication_specs {
1674-
region_configs {
1675-
priority = 7
1676-
provider_name = "AWS"
1677-
region_name = "US_EAST_1"
1678-
electable_specs {
1679-
node_count = 3
1680-
instance_size = "M10"
1681-
}
1682-
}
1683-
%[3]s
1684-
}
1685-
}
1686-
`, projectID, name, zoneNameLine) + dataSourcesTFNewSchema
1687-
}
1688-
16891662
func checksBasicDedicated(projectID, name string) resource.TestCheckFunc {
16901663
originalChecks := checkTenant(true, projectID, name)
16911664
checkMap := map[string]string{

internal/service/encryptionatrest/data_source_schema.go

+16-10
Original file line numberDiff line numberDiff line change
@@ -139,24 +139,30 @@ func DataSourceSchema(ctx context.Context) schema.Schema {
139139
"id": schema.StringAttribute{
140140
Computed: true,
141141
},
142+
"enabled_for_search_nodes": schema.BoolAttribute{
143+
Computed: true,
144+
MarkdownDescription: "Flag that indicates whether Encryption at Rest for Dedicated Search Nodes is enabled in the specified project.",
145+
},
142146
},
143147
}
144148
}
145149

146150
type TFEncryptionAtRestDSModel struct {
147-
AzureKeyVaultConfig *TFAzureKeyVaultConfigModel `tfsdk:"azure_key_vault_config"`
148-
AwsKmsConfig *TFAwsKmsConfigModel `tfsdk:"aws_kms_config"`
149-
GoogleCloudKmsConfig *TFGcpKmsConfigModel `tfsdk:"google_cloud_kms_config"`
150-
ID types.String `tfsdk:"id"`
151-
ProjectID types.String `tfsdk:"project_id"`
151+
AzureKeyVaultConfig *TFAzureKeyVaultConfigModel `tfsdk:"azure_key_vault_config"`
152+
AwsKmsConfig *TFAwsKmsConfigModel `tfsdk:"aws_kms_config"`
153+
GoogleCloudKmsConfig *TFGcpKmsConfigModel `tfsdk:"google_cloud_kms_config"`
154+
ID types.String `tfsdk:"id"`
155+
ProjectID types.String `tfsdk:"project_id"`
156+
EnabledForSearchNodes types.Bool `tfsdk:"enabled_for_search_nodes"`
152157
}
153158

154159
func NewTFEncryptionAtRestDSModel(projectID string, encryptionResp *admin.EncryptionAtRest) *TFEncryptionAtRestDSModel {
155160
return &TFEncryptionAtRestDSModel{
156-
ID: types.StringValue(projectID),
157-
ProjectID: types.StringValue(projectID),
158-
AwsKmsConfig: NewTFAwsKmsConfigItem(encryptionResp.AwsKms),
159-
AzureKeyVaultConfig: NewTFAzureKeyVaultConfigItem(encryptionResp.AzureKeyVault),
160-
GoogleCloudKmsConfig: NewTFGcpKmsConfigItem(encryptionResp.GoogleCloudKms),
161+
ID: types.StringValue(projectID),
162+
ProjectID: types.StringValue(projectID),
163+
AwsKmsConfig: NewTFAwsKmsConfigItem(encryptionResp.AwsKms),
164+
AzureKeyVaultConfig: NewTFAzureKeyVaultConfigItem(encryptionResp.AzureKeyVault),
165+
GoogleCloudKmsConfig: NewTFGcpKmsConfigItem(encryptionResp.GoogleCloudKms),
166+
EnabledForSearchNodes: types.BoolPointerValue(encryptionResp.EnabledForSearchNodes),
161167
}
162168
}

internal/service/encryptionatrest/model.go

+26-5
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,17 @@ import (
1111
)
1212

1313
func NewTFEncryptionAtRestRSModel(ctx context.Context, projectID string, encryptionResp *admin.EncryptionAtRest) *TfEncryptionAtRestRSModel {
14+
enabledForSearchNodes := false
15+
if encryptionResp.EnabledForSearchNodes != nil {
16+
enabledForSearchNodes = encryptionResp.GetEnabledForSearchNodes()
17+
}
1418
return &TfEncryptionAtRestRSModel{
15-
ID: types.StringValue(projectID),
16-
ProjectID: types.StringValue(projectID),
17-
AwsKmsConfig: NewTFAwsKmsConfig(ctx, encryptionResp.AwsKms),
18-
AzureKeyVaultConfig: NewTFAzureKeyVaultConfig(ctx, encryptionResp.AzureKeyVault),
19-
GoogleCloudKmsConfig: NewTFGcpKmsConfig(ctx, encryptionResp.GoogleCloudKms),
19+
ID: types.StringValue(projectID),
20+
ProjectID: types.StringValue(projectID),
21+
AwsKmsConfig: NewTFAwsKmsConfig(ctx, encryptionResp.AwsKms),
22+
AzureKeyVaultConfig: NewTFAzureKeyVaultConfig(ctx, encryptionResp.AzureKeyVault),
23+
GoogleCloudKmsConfig: NewTFGcpKmsConfig(ctx, encryptionResp.GoogleCloudKms),
24+
EnabledForSearchNodes: types.BoolValue(enabledForSearchNodes),
2025
}
2126
}
2227

@@ -151,3 +156,19 @@ func NewAtlasAzureKeyVault(tfAzKeyVaultConfigSlice []TFAzureKeyVaultConfigModel)
151156
RequirePrivateNetworking: v.RequirePrivateNetworking.ValueBoolPointer(),
152157
}
153158
}
159+
160+
func NewAtlasEncryptionAtRest(encryptionAtRestPlan, encryptionAtRestState *TfEncryptionAtRestRSModel, atlasEncryptionAtRest *admin.EncryptionAtRest) *admin.EncryptionAtRest {
161+
if hasAwsKmsConfigChanged(encryptionAtRestPlan.AwsKmsConfig, encryptionAtRestState.AwsKmsConfig) {
162+
atlasEncryptionAtRest.AwsKms = NewAtlasAwsKms(encryptionAtRestPlan.AwsKmsConfig)
163+
}
164+
if hasAzureKeyVaultConfigChanged(encryptionAtRestPlan.AzureKeyVaultConfig, encryptionAtRestState.AzureKeyVaultConfig) {
165+
atlasEncryptionAtRest.AzureKeyVault = NewAtlasAzureKeyVault(encryptionAtRestPlan.AzureKeyVaultConfig)
166+
}
167+
if hasGcpKmsConfigChanged(encryptionAtRestPlan.GoogleCloudKmsConfig, encryptionAtRestState.GoogleCloudKmsConfig) {
168+
atlasEncryptionAtRest.GoogleCloudKms = NewAtlasGcpKms(encryptionAtRestPlan.GoogleCloudKmsConfig)
169+
}
170+
if encryptionAtRestPlan.EnabledForSearchNodes != encryptionAtRestState.EnabledForSearchNodes {
171+
atlasEncryptionAtRest.EnabledForSearchNodes = encryptionAtRestPlan.EnabledForSearchNodes.ValueBoolPointer()
172+
}
173+
return atlasEncryptionAtRest
174+
}

internal/service/encryptionatrest/model_test.go

+10-8
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@ var (
8383
ServiceAccountKey: types.StringValue(serviceAccountKey),
8484
}
8585
EncryptionAtRest = &admin.EncryptionAtRest{
86-
AwsKms: AWSKMSConfiguration,
87-
AzureKeyVault: AzureKeyVault,
88-
GoogleCloudKms: GoogleCloudKMS,
86+
AwsKms: AWSKMSConfiguration,
87+
AzureKeyVault: AzureKeyVault,
88+
GoogleCloudKms: GoogleCloudKMS,
89+
EnabledForSearchNodes: &enabled,
8990
}
9091
)
9192

@@ -99,11 +100,12 @@ func TestNewTfEncryptionAtRestRSModel(t *testing.T) {
99100
name: "Success NewTFAwsKmsConfig",
100101
sdkModel: EncryptionAtRest,
101102
expectedResult: &encryptionatrest.TfEncryptionAtRestRSModel{
102-
ID: types.StringValue(projectID),
103-
ProjectID: types.StringValue(projectID),
104-
AwsKmsConfig: []encryptionatrest.TFAwsKmsConfigModel{TfAwsKmsConfigModel},
105-
AzureKeyVaultConfig: []encryptionatrest.TFAzureKeyVaultConfigModel{TfAzureKeyVaultConfigModel},
106-
GoogleCloudKmsConfig: []encryptionatrest.TFGcpKmsConfigModel{TfGcpKmsConfigModel},
103+
ID: types.StringValue(projectID),
104+
ProjectID: types.StringValue(projectID),
105+
AwsKmsConfig: []encryptionatrest.TFAwsKmsConfigModel{TfAwsKmsConfigModel},
106+
AzureKeyVaultConfig: []encryptionatrest.TFAzureKeyVaultConfigModel{TfAzureKeyVaultConfigModel},
107+
GoogleCloudKmsConfig: []encryptionatrest.TFGcpKmsConfigModel{TfGcpKmsConfigModel},
108+
EnabledForSearchNodes: types.BoolValue(enabled),
107109
},
108110
},
109111
}

internal/service/encryptionatrest/resource.go

+18-16
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/hashicorp/terraform-plugin-framework/path"
1515
"github.com/hashicorp/terraform-plugin-framework/resource"
1616
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
17+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
1718
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
1819
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
1920
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
@@ -52,11 +53,12 @@ type encryptionAtRestRS struct {
5253
}
5354

5455
type TfEncryptionAtRestRSModel struct {
55-
ID types.String `tfsdk:"id"`
56-
ProjectID types.String `tfsdk:"project_id"`
57-
AwsKmsConfig []TFAwsKmsConfigModel `tfsdk:"aws_kms_config"`
58-
AzureKeyVaultConfig []TFAzureKeyVaultConfigModel `tfsdk:"azure_key_vault_config"`
59-
GoogleCloudKmsConfig []TFGcpKmsConfigModel `tfsdk:"google_cloud_kms_config"`
56+
ID types.String `tfsdk:"id"`
57+
ProjectID types.String `tfsdk:"project_id"`
58+
AwsKmsConfig []TFAwsKmsConfigModel `tfsdk:"aws_kms_config"`
59+
AzureKeyVaultConfig []TFAzureKeyVaultConfigModel `tfsdk:"azure_key_vault_config"`
60+
GoogleCloudKmsConfig []TFGcpKmsConfigModel `tfsdk:"google_cloud_kms_config"`
61+
EnabledForSearchNodes types.Bool `tfsdk:"enabled_for_search_nodes"`
6062
}
6163

6264
type TFAwsKmsConfigModel struct {
@@ -105,6 +107,12 @@ func (r *encryptionAtRestRS) Schema(ctx context.Context, req resource.SchemaRequ
105107
},
106108
MarkdownDescription: "Unique 24-hexadecimal digit string that identifies your project.",
107109
},
110+
"enabled_for_search_nodes": schema.BoolAttribute{
111+
Optional: true,
112+
Computed: true,
113+
Default: booldefault.StaticBool(false),
114+
MarkdownDescription: "Flag that indicates whether Encryption at Rest for Dedicated Search Nodes is enabled in the specified project.",
115+
},
108116
},
109117
Blocks: map[string]schema.Block{
110118
"aws_kms_config": schema.ListNestedBlock{
@@ -272,6 +280,9 @@ func (r *encryptionAtRestRS) Create(ctx context.Context, req resource.CreateRequ
272280

273281
projectID := encryptionAtRestPlan.ProjectID.ValueString()
274282
encryptionAtRestReq := &admin.EncryptionAtRest{}
283+
if !encryptionAtRestPlan.EnabledForSearchNodes.IsNull() {
284+
encryptionAtRestReq.EnabledForSearchNodes = encryptionAtRestPlan.EnabledForSearchNodes.ValueBoolPointer()
285+
}
275286
if encryptionAtRestPlan.AwsKmsConfig != nil {
276287
encryptionAtRestReq.AwsKms = NewAtlasAwsKms(encryptionAtRestPlan.AwsKmsConfig)
277288
}
@@ -398,17 +409,8 @@ func (r *encryptionAtRestRS) Update(ctx context.Context, req resource.UpdateRequ
398409
return
399410
}
400411

401-
if hasAwsKmsConfigChanged(encryptionAtRestPlan.AwsKmsConfig, encryptionAtRestState.AwsKmsConfig) {
402-
atlasEncryptionAtRest.AwsKms = NewAtlasAwsKms(encryptionAtRestPlan.AwsKmsConfig)
403-
}
404-
if hasAzureKeyVaultConfigChanged(encryptionAtRestPlan.AzureKeyVaultConfig, encryptionAtRestState.AzureKeyVaultConfig) {
405-
atlasEncryptionAtRest.AzureKeyVault = NewAtlasAzureKeyVault(encryptionAtRestPlan.AzureKeyVaultConfig)
406-
}
407-
if hasGcpKmsConfigChanged(encryptionAtRestPlan.GoogleCloudKmsConfig, encryptionAtRestState.GoogleCloudKmsConfig) {
408-
atlasEncryptionAtRest.GoogleCloudKms = NewAtlasGcpKms(encryptionAtRestPlan.GoogleCloudKmsConfig)
409-
}
410-
411-
encryptionResp, _, err := connV2.EncryptionAtRestUsingCustomerKeyManagementApi.UpdateEncryptionAtRest(ctx, projectID, atlasEncryptionAtRest).Execute()
412+
updateReq := NewAtlasEncryptionAtRest(encryptionAtRestPlan, encryptionAtRestState, atlasEncryptionAtRest)
413+
encryptionResp, _, err := connV2.EncryptionAtRestUsingCustomerKeyManagementApi.UpdateEncryptionAtRest(ctx, projectID, updateReq).Execute()
412414
if err != nil {
413415
resp.Diagnostics.AddError("error updating encryption at rest", fmt.Sprintf(errorUpdateEncryptionAtRest, err.Error()))
414416
return

0 commit comments

Comments
 (0)