-
Notifications
You must be signed in to change notification settings - Fork 1
/
version_resource.go
461 lines (415 loc) · 14 KB
/
version_resource.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
package readme
import (
"context"
"errors"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/liveoaklabs/readme-api-go-client/readme"
)
// Ensure the implementation satisfies the expected interfaces.
var (
_ resource.Resource = &versionResource{}
_ resource.ResourceWithConfigure = &versionResource{}
_ resource.ResourceWithImportState = &versionResource{}
)
// versionResource is the data source implementation.
type versionResource struct {
client *readme.Client
}
// versionResourceModel maps the struct from the ReadMe client library to Terraform resource attributes.
type versionResourceModel struct {
Categories types.List `tfsdk:"categories"`
Codename types.String `tfsdk:"codename"`
CreatedAt types.String `tfsdk:"created_at"`
From types.String `tfsdk:"from"`
ForkedFrom types.String `tfsdk:"forked_from"`
ID types.String `tfsdk:"id"`
IsBeta types.Bool `tfsdk:"is_beta"`
IsDeprecated types.Bool `tfsdk:"is_deprecated"`
IsHidden types.Bool `tfsdk:"is_hidden"`
IsStable types.Bool `tfsdk:"is_stable"`
Project types.String `tfsdk:"project"`
ReleaseDate types.String `tfsdk:"release_date"`
Version types.String `tfsdk:"version"`
VersionClean types.String `tfsdk:"version_clean"`
}
// NewVersionResource is a helper function to simplify the provider implementation.
func NewVersionResource() resource.Resource {
return &versionResource{}
}
// Metadata returns the data source type name.
func (r *versionResource) Metadata(
_ context.Context,
req resource.MetadataRequest,
resp *resource.MetadataResponse,
) {
resp.TypeName = req.ProviderTypeName + "_version"
}
// Configure adds the provider configured client to the data source.
func (r *versionResource) Configure(
_ context.Context,
req resource.ConfigureRequest,
_ *resource.ConfigureResponse,
) {
if req.ProviderData == nil {
return
}
r.client = req.ProviderData.(*readme.Client)
}
// ValidateConfig is used for validating attribute values.
func (r versionResource) ValidateConfig(
ctx context.Context,
req resource.ValidateConfigRequest,
resp *resource.ValidateConfigResponse,
) {
var data versionResourceModel
resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if data.IsStable.ValueBool() && data.IsHidden.ValueBool() {
resp.Diagnostics.AddAttributeError(
path.Root("is_hidden"),
"A stable version cannot be hidden.", "is_stable and is_hidden cannot both be true. ",
)
}
if data.IsStable.ValueBool() && data.IsDeprecated.ValueBool() {
resp.Diagnostics.AddAttributeError(
path.Root("is_deprecated"),
"A stable version cannot be deprecated.",
"is_stable and is_deprecated cannot both be true. ",
)
}
if resp.Diagnostics.HasError() {
return
}
}
// Schema defines the version resource attributes.
func (r *versionResource) Schema(
_ context.Context,
_ resource.SchemaRequest,
resp *resource.SchemaResponse,
) {
resp.Schema = schema.Schema{
Description: "Manages Versions on ReadMe.com\n\n" +
"See <https://docs.readme.com/main/reference/getversion> for more information about this API " +
"endpoint.",
Attributes: map[string]schema.Attribute{
"categories": schema.ListAttribute{
Description: "List of category IDs the version is associated with.",
Computed: true,
ElementType: types.StringType,
PlanModifiers: []planmodifier.List{
listplanmodifier.UseStateForUnknown(),
},
},
"codename": schema.StringAttribute{
Description: "Dubbed name of version.",
Computed: true,
Optional: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"created_at": schema.StringAttribute{
Description: "Timestamp of when the version was created.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"from": schema.StringAttribute{
Description: "The version this version is derived from. Note that this is only an attribute used for " +
"initial creation. The ReadMe API otherwise refers to the 'from' value as an ID tracked in the " +
"forked_from attribute. When importing a version, the from field will be created after the next " +
"apply.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"forked_from": schema.StringAttribute{
Description: "The ID of the version this version is derived from.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"id": schema.StringAttribute{
Description: "The ID of the version.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"is_beta": schema.BoolAttribute{
Description: "Toggles if the version is beta or not.",
Computed: true,
Optional: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
},
},
"is_deprecated": schema.BoolAttribute{
Description: "Toggles if the version is deprecated or not.",
Computed: true,
Optional: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
},
},
"is_hidden": schema.BoolAttribute{
Description: "Toggles if the version is hidden or not. A project's stable version cannot be " +
"set to hidden.",
Computed: true,
Optional: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
},
},
"is_stable": schema.BoolAttribute{
Description: "Toggles if the version is stable. A project can only have a single stable version. " +
"Changing a stable version to non-stable will trigger a replacement. " +
"The main 'stable' version for a project cannot be deleted.",
Computed: true,
Optional: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
boolplanmodifier.RequiresReplaceIf(
func(
ctx context.Context,
req planmodifier.BoolRequest,
resp *boolplanmodifier.RequiresReplaceIfFuncResponse,
) {
// If changed from true to false, require a replacement.
if req.StateValue.ValueBool() && !req.PlanValue.ValueBool() {
resp.RequiresReplace = true
}
}, "", ""),
},
},
"project": schema.StringAttribute{
Description: "The project the version is in.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"release_date": schema.StringAttribute{
Description: "Timestamp of when the version was released.",
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"version": schema.StringAttribute{
Description: "The version string, usually a semantic version.",
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"version_clean": schema.StringAttribute{
Description: "A 'clean' version string with certain characters replaced, usually a semantic version.",
Computed: true,
PlanModifiers: []planmodifier.String{
changedIfOther(path.Root("version")),
},
},
},
}
}
// Create a version and set the initial Terraform state.
func (r *versionResource) Create(
ctx context.Context,
req resource.CreateRequest,
resp *resource.CreateResponse,
) {
// Retrieve values from plan.
var plan versionResourceModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Create the version.
plan, err := r.save("create", plan)
if err != nil {
resp.Diagnostics.AddError("Unable to create version.", err.Error())
return
}
// Set state to fully populated data.
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
// Read the remote state and refresh the Terraform state with the latest data.
func (r *versionResource) Read(
ctx context.Context,
req resource.ReadRequest,
resp *resource.ReadResponse,
) {
// Get current state.
var plan, state versionResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
resp.Diagnostics.Append(req.State.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
// Get version metadata.
state, apiResponse, err := r.get(plan.VersionClean.ValueString(), plan)
if err != nil {
if apiResponse.APIErrorResponse.Error == "VERSION_NOTFOUND" {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("Unable to read version metadata.", err.Error())
return
}
// Set refreshed state.
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
}
// Update an existing version and set the updated Terraform state on success.
func (r *versionResource) Update(
ctx context.Context,
req resource.UpdateRequest,
resp *resource.UpdateResponse,
) {
// Retrieve values from plan and current state.
var plan, state versionResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
// Update the version.
plan, err := r.save("update", plan, state.VersionClean.ValueString())
if err != nil {
resp.Diagnostics.AddError("Unable to update version.", err.Error())
return
}
// Set refreshed state.
resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
if resp.Diagnostics.HasError() {
return
}
}
// Delete a version and remove the Terraform state on success.
func (r *versionResource) Delete(
ctx context.Context,
req resource.DeleteRequest,
resp *resource.DeleteResponse,
) {
// Retrieve values from state.
var state versionResourceModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
// Delete the version.
_, apiResponse, err := r.client.Version.Delete(state.VersionClean.ValueString())
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Unable to delete version %s.", state.VersionClean),
clientError(err, apiResponse),
)
return
}
}
// ImportState imports a version via the 'version_clean' attribute.
func (r *versionResource) ImportState(
ctx context.Context,
req resource.ImportStateRequest,
resp *resource.ImportStateResponse,
) {
// Use the "version_clean" attribute for importing.
resource.ImportStatePassthroughID(ctx, path.Root("version_clean"), req, resp)
}
// get is a helper function for retrieving a version from ReadMe and mapping the response to the resource model.
// The resource module populated with current remote state is returned.
// A string is returned as an error that will be referenced in a caller function's resource response error.
func (r *versionResource) get(
version string,
plan versionResourceModel,
) (versionResourceModel, *readme.APIResponse, error) {
var state versionResourceModel
// Get the version from ReadMe.
response, apiResponse, err := r.client.Version.Get(version)
if err != nil {
return versionResourceModel{}, apiResponse, errors.New(clientError(err, apiResponse))
}
if response.ID == "" {
return versionResourceModel{}, apiResponse, fmt.Errorf(
"response is empty when looking up version '%s'",
version,
)
}
// Map response to model.
state = versionResourceModel{
Codename: types.StringValue(response.Codename),
CreatedAt: types.StringValue(response.CreatedAt),
ID: types.StringValue(response.ID),
ForkedFrom: types.StringValue(response.ForkedFrom),
From: plan.From,
IsBeta: types.BoolValue(response.IsBeta),
IsDeprecated: types.BoolValue(response.IsDeprecated),
IsHidden: types.BoolValue(response.IsHidden),
IsStable: types.BoolValue(response.IsStable),
Project: types.StringValue(response.Project),
ReleaseDate: types.StringValue(response.ReleaseDate),
Version: types.StringValue(response.Version),
VersionClean: types.StringValue(response.VersionClean),
}
// Map category list to Terraform schema model.
categories := make([]attr.Value, 0, len(response.Categories))
for _, cat := range response.Categories {
categories = append(categories, types.StringValue(cat))
}
state.Categories, _ = types.ListValue(types.StringType, categories)
return state, apiResponse, nil
}
// save is a helper function to create or update a version.
// The version is returned as a versionResourceModel.
// A string is returned in the second position for an error message that the caller function references in its
// response.
func (r *versionResource) save(
action string,
plan versionResourceModel,
version ...string,
) (versionResourceModel, error) {
var createdVersion readme.Version
var err error
var apiResponse *readme.APIResponse
createParams := readme.VersionParams{
Codename: plan.Codename.ValueString(),
From: plan.From.ValueString(),
IsBeta: plan.IsBeta.ValueBoolPointer(),
IsDeprecated: plan.IsDeprecated.ValueBoolPointer(),
IsHidden: plan.IsHidden.ValueBoolPointer(),
IsStable: plan.IsStable.ValueBoolPointer(),
Version: plan.Version.ValueString(),
}
if action == "update" {
createdVersion, apiResponse, err = r.client.Version.Update(version[0], createParams)
} else {
createdVersion, apiResponse, err = r.client.Version.Create(createParams)
}
if err != nil {
return versionResourceModel{}, errors.New(clientError(err, apiResponse))
}
plan, _, err = r.get(createdVersion.VersionClean, plan)
if err != nil {
return plan, err
}
return plan, nil
}