Skip to content

Commit ebe95d4

Browse files
feat: new mongodbatlas_stream_instance resource (#1685)
* feat: new mongodbatlas_stream_instance resource implementation * unit testing for sdk to TF model * unit testing for tf to sdk models * include acceptance tests * add test case for potential empty fields * resource documentation * include example section * configure acceptance tests in CI * include missing step for terraform setup * Update website/docs/r/stream_instance.html.markdown Co-authored-by: Andrea Angiolillo <[email protected]> * remove redundant return statement in import function * addressing doc comments --------- Co-authored-by: Andrea Angiolillo <[email protected]>
1 parent a8dfd1b commit ebe95d4

File tree

12 files changed

+690
-0
lines changed

12 files changed

+690
-0
lines changed

.github/workflows/acceptance-tests.yml

+27
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ jobs:
3535
advanced_cluster: ${{ steps.filter.outputs.advanced_cluster }}
3636
cluster: ${{ steps.filter.outputs.cluster }}
3737
search_deployment: ${{ steps.filter.outputs.search_deployment }}
38+
stream_instance: ${{ steps.filter.outputs.stream_instance }}
3839
generic: ${{ steps.filter.outputs.generic }}
3940
backup_online_archive: ${{ steps.filter.outputs.backup_online_archive }}
4041
backup_snapshots: ${{ steps.filter.outputs.backup_snapshots }}
@@ -64,6 +65,8 @@ jobs:
6465
- 'internal/service/cluster/*.go'
6566
search_deployment:
6667
- 'internal/service/searchdeployment/*.go'
68+
stream_instance:
69+
- 'internal/service/streaminstance/*.go'
6770
generic:
6871
- 'internal/service/backupcompliancepolicy/*.go'
6972
- 'internal/service/auditing/*.go'
@@ -210,6 +213,30 @@ jobs:
210213
TEST_REGEX: "^TestAccSearchDeployment"
211214
run: make testacc
212215

216+
stream_instance:
217+
needs: [ change-detection ]
218+
if: ${{ needs.change-detection.outputs.stream_instance == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.event.label.name == 'run-testacc' || github.event.label.name == 'run-testacc-stream-instance' }}
219+
runs-on: ubuntu-latest
220+
steps:
221+
- name: Checkout
222+
uses: actions/checkout@v4
223+
- name: Set up Go
224+
uses: actions/setup-go@v4
225+
with:
226+
go-version-file: 'go.mod'
227+
- uses: hashicorp/setup-terraform@v3
228+
with:
229+
terraform_version: ${{ env.terraform_version }}
230+
terraform_wrapper: false
231+
- name: Acceptance Tests
232+
env:
233+
MONGODB_ATLAS_PUBLIC_KEY: ${{ secrets.MONGODB_ATLAS_PUBLIC_KEY_CLOUD_DEV }}
234+
MONGODB_ATLAS_PRIVATE_KEY: ${{ secrets.MONGODB_ATLAS_PRIVATE_KEY_CLOUD_DEV }}
235+
MONGODB_ATLAS_ORG_ID: ${{ vars.MONGODB_ATLAS_ORG_ID_CLOUD_DEV }}
236+
MONGODB_ATLAS_BASE_URL: ${{ vars.MONGODB_ATLAS_BASE_URL }}
237+
TEST_REGEX: "^TestAccStreamInstance"
238+
run: make testacc
239+
213240
generic: # Acceptance tests that do not use any time-consuming resource (example: cluster)
214241
needs: [ change-detection ]
215242
if: ${{ needs.change-detection.outputs.generic == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' || github.event.label.name == 'run-testacc' || github.event.label.name == 'run-testacc-generic' }}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# MongoDB Atlas Provider - Atlas Stream Instance defined in a Project
2+
3+
This example shows how to use Atlas Stream Instances in Terraform. It also creates a project, which is a prerequisite.
4+
5+
You must set the following variables:
6+
7+
- `public_key`: Atlas public key
8+
- `private_key`: Atlas private key
9+
- `org_id`: Unique 24-hexadecimal digit string that identifies the Organization that must contain the project.
10+
11+
To learn more, see the [Stream Instance Documentation](https://www.mongodb.com/docs/atlas/atlas-sp/manage-processing-instance/#configure-a-stream-processing-instance).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
resource "mongodbatlas_project" "example" {
2+
name = "project-name"
3+
org_id = var.org_id
4+
}
5+
6+
resource "mongodbatlas_stream_instance" "example" {
7+
project_id = mongodbatlas_project.example
8+
instance_name = "InstanceName"
9+
data_process_region = {
10+
region = "VIRGINIA_USA"
11+
cloud_provider = "AWS"
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
provider "mongodbatlas" {
2+
public_key = var.public_key
3+
private_key = var.private_key
4+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
variable "public_key" {
2+
description = "Public API key to authenticate to Atlas"
3+
type = string
4+
}
5+
variable "private_key" {
6+
description = "Private API key to authenticate to Atlas"
7+
type = string
8+
}
9+
variable "org_id" {
10+
description = "Unique 24-hexadecimal digit string that identifies your Atlas Organization"
11+
type = string
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
terraform {
2+
required_providers {
3+
mongodbatlas = {
4+
source = "mongodb/mongodbatlas"
5+
version = "~> 1.14"
6+
}
7+
}
8+
required_version = ">= 1.0"
9+
}

internal/provider/provider.go

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/project"
3434
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/projectipaccesslist"
3535
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/searchdeployment"
36+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/streaminstance"
3637

3738
"github.com/mongodb/terraform-provider-mongodbatlas/version"
3839
)
@@ -423,6 +424,7 @@ func (p *MongodbtlasProvider) Resources(context.Context) []func() resource.Resou
423424
alertconfiguration.Resource,
424425
projectipaccesslist.Resource,
425426
searchdeployment.Resource,
427+
streaminstance.Resource,
426428
}
427429
}
428430

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package streaminstance
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/diag"
7+
"github.com/hashicorp/terraform-plugin-framework/types"
8+
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
9+
"go.mongodb.org/atlas-sdk/v20231115002/admin"
10+
)
11+
12+
func NewStreamInstanceCreateReq(ctx context.Context, plan *TFStreamInstanceRSModel) (*admin.StreamsTenant, diag.Diagnostics) {
13+
dataProcessRegion := &TFInstanceProcessRegionSpecModel{}
14+
if diags := plan.DataProcessRegion.As(ctx, dataProcessRegion, basetypes.ObjectAsOptions{}); diags.HasError() {
15+
return nil, diags
16+
}
17+
return &admin.StreamsTenant{
18+
GroupId: plan.ProjectID.ValueStringPointer(),
19+
Name: plan.InstanceName.ValueStringPointer(),
20+
DataProcessRegion: &admin.StreamsDataProcessRegion{
21+
CloudProvider: dataProcessRegion.CloudProvider.ValueString(),
22+
Region: dataProcessRegion.Region.ValueString(),
23+
},
24+
}, nil
25+
}
26+
27+
func NewStreamInstanceUpdateReq(ctx context.Context, plan *TFStreamInstanceRSModel) (*admin.StreamsDataProcessRegion, diag.Diagnostics) {
28+
dataProcessRegion := &TFInstanceProcessRegionSpecModel{}
29+
if diags := plan.DataProcessRegion.As(ctx, dataProcessRegion, basetypes.ObjectAsOptions{}); diags.HasError() {
30+
return nil, diags
31+
}
32+
return &admin.StreamsDataProcessRegion{
33+
CloudProvider: dataProcessRegion.CloudProvider.ValueString(),
34+
Region: dataProcessRegion.Region.ValueString(),
35+
}, nil
36+
}
37+
38+
func NewTFStreamInstance(ctx context.Context, apiResp *admin.StreamsTenant) (*TFStreamInstanceRSModel, diag.Diagnostics) {
39+
hostnames, diags := types.ListValueFrom(ctx, types.StringType, apiResp.Hostnames)
40+
41+
var dataProcessRegion = types.ObjectNull(ProcessRegionObjectType.AttrTypes)
42+
if apiResp.DataProcessRegion != nil {
43+
returnedProcessRegion, diagsProcessRegion := types.ObjectValueFrom(ctx, ProcessRegionObjectType.AttrTypes, TFInstanceProcessRegionSpecModel{
44+
CloudProvider: types.StringValue(apiResp.DataProcessRegion.CloudProvider),
45+
Region: types.StringValue(apiResp.DataProcessRegion.Region),
46+
})
47+
dataProcessRegion = returnedProcessRegion
48+
diags.Append(diagsProcessRegion...)
49+
}
50+
if diags.HasError() {
51+
return nil, diags
52+
}
53+
54+
return &TFStreamInstanceRSModel{
55+
ID: types.StringPointerValue(apiResp.Id),
56+
InstanceName: types.StringPointerValue(apiResp.Name),
57+
ProjectID: types.StringPointerValue(apiResp.GroupId),
58+
DataProcessRegion: dataProcessRegion,
59+
Hostnames: hostnames,
60+
}, nil
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package streaminstance_test
2+
3+
import (
4+
"context"
5+
"reflect"
6+
"testing"
7+
8+
"github.com/hashicorp/terraform-plugin-framework/types"
9+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/streaminstance"
10+
"go.mongodb.org/atlas-sdk/v20231115002/admin"
11+
)
12+
13+
type sdkToTFModelTestCase struct {
14+
SDKResp *admin.StreamsTenant
15+
expectedTFModel *streaminstance.TFStreamInstanceRSModel
16+
name string
17+
}
18+
19+
const (
20+
dummyProjectID = "111111111111111111111111"
21+
dummyStreamInstanceID = "222222222222222222222222"
22+
cloudProvider = "AWS"
23+
region = "VIRGINIA_USA"
24+
instanceName = "InstanceName"
25+
)
26+
27+
var hostnames = []string{"atlas-stream.virginia-usa.a.query.mongodb-dev.net"}
28+
29+
func TestStreamInstanceSDKToTFModel(t *testing.T) {
30+
testCases := []sdkToTFModelTestCase{
31+
{
32+
name: "Complete SDK response",
33+
SDKResp: &admin.StreamsTenant{
34+
Id: admin.PtrString(dummyStreamInstanceID),
35+
DataProcessRegion: &admin.StreamsDataProcessRegion{
36+
CloudProvider: cloudProvider,
37+
Region: region,
38+
},
39+
GroupId: admin.PtrString(dummyProjectID),
40+
Hostnames: hostnames,
41+
Name: admin.PtrString(instanceName),
42+
},
43+
expectedTFModel: &streaminstance.TFStreamInstanceRSModel{
44+
ID: types.StringValue(dummyStreamInstanceID),
45+
DataProcessRegion: tfRegionObject(t, cloudProvider, region),
46+
ProjectID: types.StringValue(dummyProjectID),
47+
Hostnames: tfHostnamesList(t, hostnames),
48+
InstanceName: types.StringValue(instanceName),
49+
},
50+
},
51+
{
52+
name: "Empty hostnames and dataProcessRegion in response", // should never happen, but verifying it is handled gracefully
53+
SDKResp: &admin.StreamsTenant{
54+
Id: admin.PtrString(dummyStreamInstanceID),
55+
GroupId: admin.PtrString(dummyProjectID),
56+
Name: admin.PtrString(instanceName),
57+
},
58+
expectedTFModel: &streaminstance.TFStreamInstanceRSModel{
59+
ID: types.StringValue(dummyStreamInstanceID),
60+
DataProcessRegion: types.ObjectNull(streaminstance.ProcessRegionObjectType.AttrTypes),
61+
ProjectID: types.StringValue(dummyProjectID),
62+
Hostnames: types.ListNull(types.StringType),
63+
InstanceName: types.StringValue(instanceName),
64+
},
65+
},
66+
}
67+
68+
for _, tc := range testCases {
69+
t.Run(tc.name, func(t *testing.T) {
70+
resultModel, diags := streaminstance.NewTFStreamInstance(context.Background(), tc.SDKResp)
71+
if diags.HasError() {
72+
t.Errorf("unexpected errors found: %s", diags.Errors()[0].Summary())
73+
}
74+
if !reflect.DeepEqual(resultModel, tc.expectedTFModel) {
75+
t.Errorf("created terraform model did not match expected output")
76+
}
77+
})
78+
}
79+
}
80+
81+
type tfToSDKCreateModelTestCase struct {
82+
tfModel *streaminstance.TFStreamInstanceRSModel
83+
expectedSDKReq *admin.StreamsTenant
84+
name string
85+
}
86+
87+
func TestStreamInstanceTFToSDKCreateModel(t *testing.T) {
88+
testCases := []tfToSDKCreateModelTestCase{
89+
{
90+
name: "Complete TF state",
91+
tfModel: &streaminstance.TFStreamInstanceRSModel{
92+
DataProcessRegion: tfRegionObject(t, cloudProvider, region),
93+
ProjectID: types.StringValue(dummyProjectID),
94+
InstanceName: types.StringValue(instanceName),
95+
},
96+
expectedSDKReq: &admin.StreamsTenant{
97+
DataProcessRegion: &admin.StreamsDataProcessRegion{
98+
CloudProvider: cloudProvider,
99+
Region: region,
100+
},
101+
GroupId: admin.PtrString(dummyProjectID),
102+
Name: admin.PtrString(instanceName),
103+
},
104+
},
105+
}
106+
107+
for _, tc := range testCases {
108+
t.Run(tc.name, func(t *testing.T) {
109+
apiReqResult, diags := streaminstance.NewStreamInstanceCreateReq(context.Background(), tc.tfModel)
110+
if diags.HasError() {
111+
t.Errorf("unexpected errors found: %s", diags.Errors()[0].Summary())
112+
}
113+
if !reflect.DeepEqual(apiReqResult, tc.expectedSDKReq) {
114+
t.Errorf("created sdk model did not match expected output")
115+
}
116+
})
117+
}
118+
}
119+
120+
type tfToSDKUpdateModelTestCase struct {
121+
tfModel *streaminstance.TFStreamInstanceRSModel
122+
expectedSDKReq *admin.StreamsDataProcessRegion
123+
name string
124+
}
125+
126+
func TestStreamInstanceTFToSDKUpdateModel(t *testing.T) {
127+
testCases := []tfToSDKUpdateModelTestCase{
128+
{
129+
name: "Complete TF state",
130+
tfModel: &streaminstance.TFStreamInstanceRSModel{
131+
ID: types.StringValue(dummyStreamInstanceID),
132+
DataProcessRegion: tfRegionObject(t, cloudProvider, region),
133+
ProjectID: types.StringValue(dummyProjectID),
134+
Hostnames: tfHostnamesList(t, hostnames),
135+
InstanceName: types.StringValue(instanceName),
136+
},
137+
expectedSDKReq: &admin.StreamsDataProcessRegion{
138+
CloudProvider: cloudProvider,
139+
Region: region,
140+
},
141+
},
142+
}
143+
144+
for _, tc := range testCases {
145+
t.Run(tc.name, func(t *testing.T) {
146+
apiReqResult, diags := streaminstance.NewStreamInstanceUpdateReq(context.Background(), tc.tfModel)
147+
if diags.HasError() {
148+
t.Errorf("unexpected errors found: %s", diags.Errors()[0].Summary())
149+
}
150+
if !reflect.DeepEqual(apiReqResult, tc.expectedSDKReq) {
151+
t.Errorf("created sdk model did not match expected output")
152+
}
153+
})
154+
}
155+
}
156+
157+
func tfRegionObject(t *testing.T, cloudProvider, region string) types.Object {
158+
dataProcessRegion, diags := types.ObjectValueFrom(context.Background(), streaminstance.ProcessRegionObjectType.AttrTypes, streaminstance.TFInstanceProcessRegionSpecModel{
159+
CloudProvider: types.StringValue(cloudProvider),
160+
Region: types.StringValue(region),
161+
})
162+
if diags.HasError() {
163+
t.Errorf("failed to create terraform data process region model: %s", diags.Errors()[0].Summary())
164+
}
165+
return dataProcessRegion
166+
}
167+
168+
func tfHostnamesList(t *testing.T, hostnames []string) types.List {
169+
resultList, diags := types.ListValueFrom(context.Background(), types.StringType, hostnames)
170+
if diags.HasError() {
171+
t.Errorf("failed to create terraform hostnames list: %s", diags.Errors()[0].Summary())
172+
}
173+
return resultList
174+
}

0 commit comments

Comments
 (0)