11package autogen
22
33import (
4+ "bytes"
45 "context"
6+ "encoding/json"
57 "fmt"
68 "io"
79 "net/http"
810 "time"
911
1012 "github.com/hashicorp/terraform-plugin-framework/diag"
1113 "github.com/hashicorp/terraform-plugin-framework/resource"
14+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
15+ "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/retrystrategy"
1216 "github.com/mongodb/terraform-provider-mongodbatlas/internal/common/validate"
1317 "github.com/mongodb/terraform-provider-mongodbatlas/internal/config"
1418)
@@ -45,27 +49,27 @@ type HandleCreateReq struct {
4549
4650func HandleCreate (ctx context.Context , req HandleCreateReq ) {
4751 d := & req .Resp .Diagnostics
48- reqBody , err := Marshal (req .Plan , false )
52+ bodyReq , err := Marshal (req .Plan , false )
4953 if err != nil {
5054 addError (d , opCreate , errBuildingAPIRequest , err )
5155 return
5256 }
53- respBody , err := callAPIWithBody (ctx , req .Client , req .CallParams , reqBody )
57+ bodyResp , err := callAPIWithBody (ctx , req .Client , req .CallParams , bodyReq )
5458 if err != nil {
5559 addError (d , opCreate , errCallingAPI , err )
5660 return
5761 }
5862
5963 // Use the plan as the base model to set the response state
60- if err := Unmarshal (respBody , req .Plan ); err != nil {
64+ if err := Unmarshal (bodyResp , req .Plan ); err != nil {
6165 addError (d , opCreate , errUnmarshallingResponse , err )
6266 return
6367 }
6468 if err := ResolveUnknowns (req .Plan ); err != nil {
6569 addError (d , opCreate , errResolvingResponse , err )
6670 return
6771 }
68- if err := handleWait (ctx , req .Wait , req .Client , req .Plan ); err != nil {
72+ if err := handleWaitCreateUpdate (ctx , req .Wait , req .Client , req .Plan ); err != nil {
6973 addError (d , opCreate , errWaitingForChanges , err )
7074 return
7175 }
@@ -81,18 +85,18 @@ type HandleReadReq struct {
8185
8286func HandleRead (ctx context.Context , req HandleReadReq ) {
8387 d := & req .Resp .Diagnostics
84- respBody , apiResp , err := callAPIWithoutBody (ctx , req .Client , req .CallParams )
88+ bodyResp , apiResp , err := callAPIWithoutBody (ctx , req .Client , req .CallParams )
89+ if notFound (bodyResp , apiResp ) {
90+ req .Resp .State .RemoveResource (ctx )
91+ return
92+ }
8593 if err != nil {
86- if validate .StatusNotFound (apiResp ) {
87- req .Resp .State .RemoveResource (ctx )
88- return
89- }
9094 addError (d , opRead , errCallingAPI , err )
9195 return
9296 }
9397
9498 // Use the current state as the base model to set the response state
95- if err := Unmarshal (respBody , req .State ); err != nil {
99+ if err := Unmarshal (bodyResp , req .State ); err != nil {
96100 addError (d , opRead , errUnmarshallingResponse , err )
97101 return
98102 }
@@ -113,27 +117,27 @@ type HandleUpdateReq struct {
113117
114118func HandleUpdate (ctx context.Context , req HandleUpdateReq ) {
115119 d := & req .Resp .Diagnostics
116- reqBody , err := Marshal (req .Plan , true )
120+ bodyReq , err := Marshal (req .Plan , true )
117121 if err != nil {
118122 addError (d , opUpdate , errBuildingAPIRequest , err )
119123 return
120124 }
121- respBody , err := callAPIWithBody (ctx , req .Client , req .CallParams , reqBody )
125+ bodyResp , err := callAPIWithBody (ctx , req .Client , req .CallParams , bodyReq )
122126 if err != nil {
123127 addError (d , opUpdate , errCallingAPI , err )
124128 return
125129 }
126130
127131 // Use the plan as the base model to set the response state
128- if err := Unmarshal (respBody , req .Plan ); err != nil {
132+ if err := Unmarshal (bodyResp , req .Plan ); err != nil {
129133 addError (d , opUpdate , errUnmarshallingResponse , err )
130134 return
131135 }
132136 if err := ResolveUnknowns (req .Plan ); err != nil {
133137 addError (d , opUpdate , errResolvingResponse , err )
134138 return
135139 }
136- if err := handleWait (ctx , req .Wait , req .Client , req .Plan ); err != nil {
140+ if err := handleWaitCreateUpdate (ctx , req .Wait , req .Client , req .Plan ); err != nil {
137141 addError (d , opUpdate , errWaitingForChanges , err )
138142 return
139143 }
@@ -154,70 +158,128 @@ func HandleDelete(ctx context.Context, req HandleDeleteReq) {
154158 addError (d , opDelete , errCallingAPI , err )
155159 return
156160 }
157- // don't consider wait errors as it can happen that the resource is already deleted
158- _ = handleWait (ctx , req .Wait , req .Client , req .State )
161+ if err := handleWaitDelete (ctx , req .Wait , req .Client ); err != nil {
162+ addError (d , opDelete , errWaitingForChanges , err )
163+ }
159164}
160165
161- // handleWait waits until a long-running operation is done if needed.
166+ // handleWaitCreateUpdate waits until a long-running operation is done if needed.
162167// It also updates the model with the latest JSON response from the API.
163- func handleWait (ctx context.Context , wait * WaitReq , client * config.MongoDBClient , model any ) error {
168+ func handleWaitCreateUpdate (ctx context.Context , wait * WaitReq , client * config.MongoDBClient , model any ) error {
164169 if wait == nil {
165170 return nil
166171 }
167- respBody , err := waitForChanges (ctx , wait , client )
168- if err != nil {
172+ bodyResp , err := waitForChanges (ctx , wait , client )
173+ if err != nil || isEmptyJSON ( bodyResp ) {
169174 return err
170175 }
171- if respBody == nil {
176+ if err := Unmarshal (bodyResp , model ); err != nil {
177+ return err
178+ }
179+ return ResolveUnknowns (model )
180+ }
181+
182+ // handleWaitDelete waits until a long-running operation to delete a resource if neeed.
183+ func handleWaitDelete (ctx context.Context , wait * WaitReq , client * config.MongoDBClient ) error {
184+ if wait == nil {
172185 return nil
173186 }
174- if err := Unmarshal ( respBody , model ); err != nil {
187+ if _ , err := waitForChanges ( ctx , wait , client ); err != nil {
175188 return err
176189 }
177- return ResolveUnknowns ( model )
190+ return nil
178191}
179192
180- // waitForChanges waits until a long-running operation is done.
181- // It returns the latest JSON response from the API so it can be used to update the response state.
182- // TODO: This is a basic implementation, it will be replaced in CLOUDP-314960.
183- func waitForChanges (ctx context.Context , wait * WaitReq , client * config.MongoDBClient ) ([]byte , error ) {
184- time .Sleep (time .Duration (wait .TimeoutSeconds ) * time .Second ) // TODO: TimeoutSeconds is temporarily used to allow time to destroy the resource until autogen long-running operations are supported in CLOUDP-314960
185- respBody , _ , err := callAPIWithoutBody (ctx , client , wait .CallParams )
186- return respBody , err
193+ func addError (d * diag.Diagnostics , opName , errSummary string , err error ) {
194+ d .AddError (fmt .Sprintf ("Error %s in %s" , errSummary , opName ), err .Error ())
187195}
188196
189197// callAPIWithBody makes a request to the API with the given request body and returns the response body.
190198// It is used for POST, PUT, and PATCH requests where a request body is required.
191- func callAPIWithBody (ctx context.Context , client * config.MongoDBClient , callParams * config.APICallParams , reqBody []byte ) ([]byte , error ) {
192- callParams .Body = reqBody
193- apiResp , err := client .UntypedAPICall (ctx , callParams )
199+ func callAPIWithBody (ctx context.Context , client * config.MongoDBClient , callParams * config.APICallParams , bodyReq []byte ) ([]byte , error ) {
200+ apiResp , err := client .UntypedAPICall (ctx , callParams , bodyReq )
194201 if err != nil {
195202 return nil , err
196203 }
197- respBody , err := io .ReadAll (apiResp .Body )
204+ bodyResp , err := io .ReadAll (apiResp .Body )
198205 apiResp .Body .Close ()
199206 if err != nil {
200207 return nil , err
201208 }
202- return respBody , nil
209+ return bodyResp , nil
203210}
204211
205212// callAPIWithoutBody makes a request to the API without a request body and returns the response body.
206213// It is used for GET or DELETE requests where no request body is required.
207214func callAPIWithoutBody (ctx context.Context , client * config.MongoDBClient , callParams * config.APICallParams ) ([]byte , * http.Response , error ) {
208- callParams .Body = nil
209- apiResp , err := client .UntypedAPICall (ctx , callParams )
215+ apiResp , err := client .UntypedAPICall (ctx , callParams , nil )
210216 if err != nil {
211217 return nil , apiResp , err
212218 }
213- respBody , err := io .ReadAll (apiResp .Body )
219+ bodyResp , err := io .ReadAll (apiResp .Body )
214220 apiResp .Body .Close ()
215221 if err != nil {
216222 return nil , apiResp , err
217223 }
218- return respBody , apiResp , nil
224+ return bodyResp , apiResp , nil
219225}
220226
221- func addError (d * diag.Diagnostics , opName , errSummary string , err error ) {
222- d .AddError (fmt .Sprintf ("Error %s in %s" , errSummary , opName ), err .Error ())
227+ // waitForChanges waits until a long-running operation is done.
228+ // It returns the latest JSON response from the API so it can be used to update the response state.
229+ func waitForChanges (ctx context.Context , wait * WaitReq , client * config.MongoDBClient ) ([]byte , error ) {
230+ if len (wait .TargetStates ) == 0 {
231+ return nil , fmt .Errorf ("wait must have at least one target state, pending states: %v" , wait .PendingStates )
232+ }
233+ stateConf := retry.StateChangeConf {
234+ Target : wait .TargetStates ,
235+ Pending : wait .PendingStates ,
236+ Timeout : time .Duration (wait .TimeoutSeconds ) * time .Second ,
237+ MinTimeout : time .Duration (wait .MinTimeoutSeconds ) * time .Second ,
238+ Delay : time .Duration (wait .DelaySeconds ) * time .Second ,
239+ Refresh : refreshFunc (ctx , wait , client ),
240+ }
241+ bodyResp , err := stateConf .WaitForStateContext (ctx )
242+ if err != nil || bodyResp == nil {
243+ return nil , err
244+ }
245+ return bodyResp .([]byte ), err
223246}
247+
248+ // refreshFunc retries until a target state or error happens.
249+ // It uses a special state value of "DELETED" when the API returns 404 or empty object
250+ func refreshFunc (ctx context.Context , wait * WaitReq , client * config.MongoDBClient ) retry.StateRefreshFunc {
251+ return func () (result any , state string , err error ) {
252+ bodyResp , httpResp , err := callAPIWithoutBody (ctx , client , wait .CallParams )
253+ if notFound (bodyResp , httpResp ) {
254+ return emptyJSON , retrystrategy .RetryStrategyDeletedState , nil
255+ }
256+ if err != nil {
257+ return nil , "" , err
258+ }
259+ var objJSON map [string ]any
260+ if err := json .Unmarshal (bodyResp , & objJSON ); err != nil {
261+ return nil , "" , err
262+ }
263+ stateValAny , found := objJSON [wait .StateAttribute ]
264+ if ! found {
265+ return nil , "" , fmt .Errorf ("wait state attribute not found: %s" , wait .StateAttribute )
266+ }
267+ stateValStr , ok := stateValAny .(string )
268+ if ! ok {
269+ return nil , "" , fmt .Errorf ("wait state attribute value is not a string, attribute name: %s, value: %s" , wait .StateAttribute , stateValAny )
270+ }
271+ return bodyResp , stateValStr , nil
272+ }
273+ }
274+
275+ // notFound returns if the resource is not found (API response is 404 or response body is empty JSON).
276+ // That is because some resources like search_deployment can return an ok status code with empty json when resource doesn't exist.
277+ func notFound (bodyResp []byte , apiResp * http.Response ) bool {
278+ return validate .StatusNotFound (apiResp ) || isEmptyJSON (bodyResp )
279+ }
280+
281+ func isEmptyJSON (raw []byte ) bool {
282+ return len (raw ) == 0 || bytes .Equal (raw , emptyJSON )
283+ }
284+
285+ var emptyJSON = []byte ("{}" )
0 commit comments