5
5
"fmt"
6
6
"slices"
7
7
"strings"
8
+ "sync"
8
9
9
10
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
10
11
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
@@ -64,11 +65,14 @@ func tf2model(tfData tfDNSRecord) (model.DNSDomain, model.DNSRecord) {
64
65
65
66
// RecordResource defines the implementation of GoDaddy DNS RR
66
67
type RecordResource struct {
67
- client model.DNSApiClient
68
+ client model.DNSApiClient
69
+ reqMutex * sync.Mutex
68
70
}
69
71
70
- func NewRecordResource () resource.Resource {
71
- return & RecordResource {}
72
+ func RecordResourceFactory (m * sync.Mutex ) func () resource.Resource {
73
+ return func () resource.Resource {
74
+ return & RecordResource {reqMutex : m }
75
+ }
72
76
}
73
77
74
78
func (r * RecordResource ) Metadata (ctx context.Context , req resource.MetadataRequest , resp * resource.MetadataResponse ) {
@@ -159,6 +163,10 @@ func (r *RecordResource) Configure(ctx context.Context, req resource.ConfigureRe
159
163
r .client = client
160
164
}
161
165
166
+ // create will complain (and fail with client error) if same record is already present
167
+ // (mb as a result of calling "apply" with updated config with old record already gone)
168
+ // so state must be manually imported to continue (could step around this, but this will
169
+ // contradict terraform ideology -- see below)
162
170
func (r * RecordResource ) Create (ctx context.Context , req resource.CreateRequest , resp * resource.CreateResponse ) {
163
171
var planData tfDNSRecord
164
172
resp .Diagnostics .Append (req .Plan .Get (ctx , & planData )... )
@@ -169,11 +177,17 @@ func (r *RecordResource) Create(ctx context.Context, req resource.CreateRequest,
169
177
ctx = setLogCtx (ctx , planData , "create" )
170
178
tflog .Info (ctx , "create: start" )
171
179
defer tflog .Info (ctx , "create: end" )
180
+ r .reqMutex .Lock ()
181
+ defer r .reqMutex .Unlock ()
172
182
173
183
apiDomain , apiRecPlan := tf2model (planData )
174
- // add: does not check (read) if creating w/o prior state
175
- // and so will fail on uniqueness violation (e.g. if CNAME already
176
- // exists, even with the same name); ok for us -- let API do checking
184
+ // "put"/"add" does not check prior state (terraform does not provide one for Create)
185
+ // and so will fail on uniqueness violation (e.g. if record already exists
186
+ // after external modification, or if it is the second CNAME RR etc)
187
+ // - lets think it is ok for now -- let API do checking + run "import" if required
188
+ // - alt/TODO: read records and do noop if target record is already there
189
+ // like `apiAllRecs, err := r.client.GetRecords(ctx, apiDomain, apiRecPlan.Type, apiRecPlan.Name)`
190
+ // but lets not be silent about that
177
191
err := r .client .AddRecords (ctx , apiDomain , []model.DNSRecord {apiRecPlan })
178
192
179
193
if err != nil {
@@ -195,6 +209,8 @@ func (r *RecordResource) Read(ctx context.Context, req resource.ReadRequest, res
195
209
ctx = setLogCtx (ctx , stateData , "read" )
196
210
tflog .Info (ctx , "read: start" )
197
211
defer tflog .Info (ctx , "read: end" )
212
+ r .reqMutex .Lock ()
213
+ defer r .reqMutex .Unlock ()
198
214
199
215
apiDomain , apiRecState := tf2model (stateData )
200
216
@@ -204,11 +220,9 @@ func (r *RecordResource) Read(ctx context.Context, req resource.ReadRequest, res
204
220
fmt .Sprintf ("Reading DNS records: query failed: %s" , err ))
205
221
return
206
222
}
223
+ numFound := 0
207
224
if numRecs := len (apiAllRecs ); numRecs == 0 {
208
225
tflog .Debug (ctx , "Reading DNS record: currently absent" )
209
- // no resource found: mb ok or need to re-create
210
- resp .State .RemoveResource (ctx )
211
- return
212
226
} else {
213
227
tflog .Info (ctx , fmt .Sprintf (
214
228
"Reading DNS record: got %d answers" , numRecs ))
@@ -221,7 +235,6 @@ func (r *RecordResource) Read(ctx context.Context, req resource.ReadRequest, res
221
235
// preversion and replace it with one :)
222
236
// - TXT and NS for same name could differ only in TTL
223
237
// - for SRV PK is proto+service+port+data, value is weight+prio+ttl
224
- numFound := 0
225
238
for _ , rec := range apiAllRecs {
226
239
tflog .Debug (ctx , fmt .Sprintf ("Got DNS record: %v" , rec ))
227
240
if rec .SameKey (apiRecState ) {
@@ -238,18 +251,31 @@ func (r *RecordResource) Read(ctx context.Context, req resource.ReadRequest, res
238
251
numFound += 1
239
252
}
240
253
}
241
- if numFound == 0 {
242
- tflog .Info (ctx , "no matching record found" )
243
- } else {
244
- if numFound > 1 {
245
- tflog .Warn (ctx , "more than one matching record found, using last" )
246
- }
247
- }
248
254
}
249
255
250
- resp .Diagnostics .Append (resp .State .Set (ctx , & stateData )... )
256
+ if numFound == 0 {
257
+ // mb quite ok, e.g. on creation
258
+ tflog .Info (ctx , "Resource is currently absent" )
259
+ resp .State .RemoveResource (ctx )
260
+ } else {
261
+ if numFound > 1 {
262
+ // unlikely to happen (mb several MXes with the same target?)
263
+ tflog .Warn (ctx , "More than one instance of a resource present" )
264
+ resp .Diagnostics .AddWarning (
265
+ "Duplicate resource instances present" ,
266
+ "Will use the last one" )
267
+ }
268
+ resp .Diagnostics .Append (resp .State .Set (ctx , & stateData )... )
269
+ }
251
270
}
252
271
272
+ // updating will fail if resource is already changed externally: old record will be "gone"
273
+ // after refresh, so actually "create" will be called for new one (and see above): i.e.
274
+ // changing "A -> 1.1.1.1" to "A -> 2.2.2.2" first in domain and then in main.tf will
275
+ // result in an error (refresh will mark it as gone and will try to create new)
276
+ // so, do not do that :)
277
+ // the way to settle things down in this case is "refresh" (will mark old as gone)
278
+ // + "import" to new (so state will be ok)
253
279
func (r * RecordResource ) Update (ctx context.Context , req resource.UpdateRequest , resp * resource.UpdateResponse ) {
254
280
var planData tfDNSRecord
255
281
resp .Diagnostics .Append (req .Plan .Get (ctx , & planData )... )
@@ -260,6 +286,8 @@ func (r *RecordResource) Update(ctx context.Context, req resource.UpdateRequest,
260
286
ctx = setLogCtx (ctx , planData , "update" )
261
287
tflog .Info (ctx , "update: start" )
262
288
defer tflog .Info (ctx , "update: end" )
289
+ r .reqMutex .Lock ()
290
+ defer r .reqMutex .Unlock ()
263
291
264
292
apiDomain , apiRecPlan := tf2model (planData )
265
293
@@ -286,20 +314,33 @@ func (r *RecordResource) Update(ctx context.Context, req resource.UpdateRequest,
286
314
fmt .Sprintf ("Getting DNS records to keep failed: %s" , err ))
287
315
return
288
316
}
317
+ // lets try to detect the situation when old record is gone and new is present
318
+ // actually this should not happen (implicit "refresh" before "apply" will remove
319
+ // old record from the state), but who knows :)
320
+ oldGone := false
321
+ if err == errRecordGone {
322
+ tflog .Info (ctx , "Current record is already gone" )
323
+ oldGone = true
324
+ }
289
325
tflog .Info (ctx , fmt .Sprintf ("Got %d records to keep" , len (apiUpdateRecs )))
290
- // and finally, our record (TODO: SRV has more fields)
326
+ // and finally, add our record (TODO: SRV has more fields)
291
327
ourRec := model.DNSUpdateRecord {
292
328
Data : apiRecPlan .Data ,
293
329
TTL : apiRecPlan .TTL ,
294
330
Priority : apiRecPlan .Priority ,
295
331
}
296
- // TODO: probably cannot be by construction :)
297
- // and comparing them this way is wrong anyway
332
+ newPresent := false
298
333
if slices .Index (apiUpdateRecs , ourRec ) >= 0 {
299
- tflog .Info (ctx , "Record is already present, nothing to do: done" )
300
- err = nil
334
+ // still need to delete old value if not gone
335
+ tflog .Info (ctx , "Updated record is already present" )
336
+ newPresent = true
301
337
} else {
302
338
apiUpdateRecs = append (apiUpdateRecs , ourRec )
339
+ }
340
+ if oldGone && newPresent {
341
+ tflog .Info (ctx , "Nothing left to do" )
342
+ err = nil
343
+ } else {
303
344
err = r .client .SetRecords (ctx , apiDomain , apiRecPlan .Type , apiRecPlan .Name , apiUpdateRecs )
304
345
}
305
346
}
@@ -324,6 +365,8 @@ func (r *RecordResource) Delete(ctx context.Context, req resource.DeleteRequest,
324
365
ctx = setLogCtx (ctx , stateData , "delete" )
325
366
tflog .Info (ctx , "delete: start" )
326
367
defer tflog .Info (ctx , "delete: end" )
368
+ r .reqMutex .Lock ()
369
+ defer r .reqMutex .Unlock ()
327
370
328
371
apiDomain , apiRecState := tf2model (stateData )
329
372
0 commit comments