Skip to content

Commit b5b459a

Browse files
committed
CLOUDP-446873: Skip connection update for alias migration
Treats an instance_name to workspace_name transition with the same effective workspace and unchanged connection settings as a state-only update. This avoids an Atlas PATCH that active processors prohibit.
1 parent 6e96f3d commit b5b459a

5 files changed

Lines changed: 111 additions & 2 deletions

File tree

.changelog/446873.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:bug
2+
Skips connection API updates when migrating between equivalent instance_name and workspace_name values
3+
```

docs/guides/stream-instance-to-stream-workspace-migration-guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ resource "mongodbatlas_stream_connection" "example" {
8686
}
8787
```
8888

89-
Use the same `instance_name` to `workspace_name` change for stream processors. When both attributes resolve to the same workspace, updating a stream connection is an in-place state migration and does not replace the connection. An alias-only stream processor update also does not stop or restart the processor.
89+
Use the same `instance_name` to `workspace_name` change for stream processors. When both attributes resolve to the same workspace, the provider updates a stream connection in Terraform state only: it does not replace the connection or send an Atlas API update. An alias-only stream processor update also does not stop or restart the processor.
9090

9191
Run `terraform plan` before applying the configuration. Expect the workspace resource to be moved and dependent resources to be updated in place; do not apply a plan that destroys and recreates an unchanged stream connection.
9292

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package streamconnection_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/hashicorp/terraform-plugin-framework/types"
7+
"github.com/stretchr/testify/assert"
8+
9+
"github.com/mongodb/terraform-provider-mongodbatlas/internal/service/streamconnection"
10+
)
11+
12+
func TestIsAliasOnlyTransition(t *testing.T) {
13+
legacyState := streamconnection.TFStreamConnectionModel{
14+
TFStreamConnectionCommonModel: streamconnection.TFStreamConnectionCommonModel{
15+
ID: types.StringValue("workspace-project-connection"),
16+
InstanceName: types.StringValue("workspace"),
17+
WorkspaceName: types.StringNull(),
18+
ProjectID: types.StringValue("project"),
19+
ConnectionName: types.StringValue("connection"),
20+
Type: types.StringValue("Sample"),
21+
},
22+
}
23+
24+
testCases := map[string]struct {
25+
plan streamconnection.TFStreamConnectionModel
26+
state streamconnection.TFStreamConnectionModel
27+
aliasOnlyTransition bool
28+
}{
29+
"legacy_to_canonical": {
30+
state: legacyState,
31+
plan: streamconnection.TFStreamConnectionModel{
32+
TFStreamConnectionCommonModel: streamconnection.TFStreamConnectionCommonModel{
33+
ID: types.StringUnknown(),
34+
InstanceName: types.StringNull(),
35+
WorkspaceName: types.StringValue("workspace"),
36+
ProjectID: types.StringValue("project"),
37+
ConnectionName: types.StringValue("connection"),
38+
Type: types.StringValue("Sample"),
39+
},
40+
},
41+
aliasOnlyTransition: true,
42+
},
43+
"different_workspace": {
44+
state: legacyState,
45+
plan: streamconnection.TFStreamConnectionModel{
46+
TFStreamConnectionCommonModel: streamconnection.TFStreamConnectionCommonModel{
47+
InstanceName: types.StringNull(),
48+
WorkspaceName: types.StringValue("other-workspace"),
49+
ProjectID: types.StringValue("project"),
50+
ConnectionName: types.StringValue("connection"),
51+
Type: types.StringValue("Sample"),
52+
},
53+
},
54+
},
55+
"connection_configuration_changed": {
56+
state: legacyState,
57+
plan: streamconnection.TFStreamConnectionModel{
58+
TFStreamConnectionCommonModel: streamconnection.TFStreamConnectionCommonModel{
59+
InstanceName: types.StringNull(),
60+
WorkspaceName: types.StringValue("workspace"),
61+
ProjectID: types.StringValue("project"),
62+
ConnectionName: types.StringValue("connection"),
63+
Type: types.StringValue("Kafka"),
64+
BootstrapServers: types.StringValue("broker:9092"),
65+
},
66+
},
67+
},
68+
}
69+
70+
for name, tc := range testCases {
71+
t.Run(name, func(t *testing.T) {
72+
assert.Equal(t, tc.aliasOnlyTransition, streamconnection.IsAliasOnlyTransition(&tc.plan, &tc.state))
73+
})
74+
}
75+
}

internal/service/streamconnection/model_stream_connection.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package streamconnection
33
import (
44
"context"
55
"fmt"
6+
"reflect"
67

78
"github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts"
89
"github.com/hashicorp/terraform-plugin-framework/diag"
@@ -13,6 +14,28 @@ import (
1314
"go.mongodb.org/atlas-sdk/v20250312025/admin"
1415
)
1516

17+
// IsAliasOnlyTransition reports whether a plan preserves the effective workspace
18+
// and changes no connection configuration other than its alias representation.
19+
func IsAliasOnlyTransition(plan, state *TFStreamConnectionModel) bool {
20+
if plan.WorkspaceName.IsUnknown() || plan.InstanceName.IsUnknown() || state.WorkspaceName.IsUnknown() || state.InstanceName.IsUnknown() {
21+
return false
22+
}
23+
if plan.WorkspaceName.Equal(state.WorkspaceName) && plan.InstanceName.Equal(state.InstanceName) {
24+
return false
25+
}
26+
if getWorkspaceOrInstanceName(plan) != getWorkspaceOrInstanceName(state) {
27+
return false
28+
}
29+
30+
planWithoutAliases := *plan
31+
planWithoutAliases.WorkspaceName = state.WorkspaceName
32+
planWithoutAliases.InstanceName = state.InstanceName
33+
// ID is computed and may be unknown in an update plan even though no
34+
// connection setting changed.
35+
planWithoutAliases.ID = state.ID
36+
return reflect.DeepEqual(planWithoutAliases, *state)
37+
}
38+
1639
func NewStreamConnectionReq(ctx context.Context, plan *TFStreamConnectionModel) (*admin.StreamsConnection, diag.Diagnostics) {
1740
streamConnection := admin.StreamsConnection{
1841
Name: plan.ConnectionName.ValueStringPointer(),

internal/service/streamconnection/resource_stream_connection.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,12 +296,20 @@ func (r *streamConnectionRS) Read(ctx context.Context, req resource.ReadRequest,
296296
}
297297

298298
func (r *streamConnectionRS) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
299-
var streamConnectionPlan TFStreamConnectionModel
299+
var streamConnectionPlan, streamConnectionState TFStreamConnectionModel
300300
resp.Diagnostics.Append(req.Plan.Get(ctx, &streamConnectionPlan)...)
301+
resp.Diagnostics.Append(req.State.Get(ctx, &streamConnectionState)...)
301302
if resp.Diagnostics.HasError() {
302303
return
303304
}
304305

306+
if IsAliasOnlyTransition(&streamConnectionPlan, &streamConnectionState) {
307+
streamConnectionState.InstanceName = streamConnectionPlan.InstanceName
308+
streamConnectionState.WorkspaceName = streamConnectionPlan.WorkspaceName
309+
resp.Diagnostics.Append(resp.State.Set(ctx, streamConnectionState)...)
310+
return
311+
}
312+
305313
connV2 := r.Client.AtlasV2
306314
projectID := streamConnectionPlan.ProjectID.ValueString()
307315
workspaceOrInstanceName := getWorkspaceOrInstanceName(&streamConnectionPlan)

0 commit comments

Comments
 (0)