/
provider.go
286 lines (242 loc) · 8.9 KB
/
provider.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
// Package readme is a Terraform provider for interacting with the ReadMe.com API.
package readme
import (
"context"
"fmt"
"os"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/liveoaklabs/readme-api-go-client/readme"
)
const (
// IDPrefix is the prefix used for ReadMe resource IDs.
IDPrefix = "id:"
// UUIDPrefix is the prefix used for ReadMe UUIDs.
UUIDPrefix = "uuid:"
)
// Ensure the implementation satisfies the expected interfaces
var _ provider.Provider = &readmeProvider{}
// New is a helper function to simplify provider server and testing implementation.
func New(version string) func() provider.Provider {
return func() provider.Provider {
return &readmeProvider{
Version: version,
}
}
}
// readmeProvider is the provider implementation.
type readmeProvider struct {
Version string
}
// readmeProviderModel maps provider schema data to a Go type.
type readmeProviderModel struct {
APIToken types.String `tfsdk:"api_token"`
APIURL types.String `tfsdk:"api_url"`
}
// saveAction is a custom type to represent the action to take when saving a
// resource.
type saveAction string
const (
saveActionCreate saveAction = "create"
saveActionUpdate saveAction = "update"
)
// Metadata returns the provider type name.
func (p *readmeProvider) Metadata(
_ context.Context,
_ provider.MetadataRequest,
resp *provider.MetadataResponse,
) {
resp.TypeName = "readme"
}
// Schema defines the provider-level schema for configuration data.
func (p *readmeProvider) Schema(
ctx context.Context,
req provider.SchemaRequest,
resp *provider.SchemaResponse,
) {
resp.Schema = schema.Schema{
Description: "The ReadMe provider provides resources and data sources for interacting with the ReadMe.com API.",
MarkdownDescription: "The ReadMe provider provides resources and data sources for interacting with the " +
"[ReadMe.com](https://docs.readme.com/main/reference/intro-to-the-readme-api) API.",
Attributes: map[string]schema.Attribute{
"api_token": schema.StringAttribute{
Description: "Client token for accessing the ReadMe API. May alternatively be set with the " +
"README_API_TOKEN environment variable.",
MarkdownDescription: "Client token for accessing the ReadMe API. May alternatively be set with the " +
"`README_API_TOKEN` environment variable.",
Optional: true,
Sensitive: true,
},
"api_url": schema.StringAttribute{
Description: "URL for accessing the ReadMe API. May also be set with the README_API_URL " +
"environment variable or left unset to use the default.",
MarkdownDescription: "URL for accessing the ReadMe API. May also be set with the `README_API_URL` " +
"environment variable or left unset to use the default.",
Optional: true,
},
},
}
}
// Configure prepares a readme API client for data sources and resources.
func (p *readmeProvider) Configure(
ctx context.Context,
req provider.ConfigureRequest,
resp *provider.ConfigureResponse,
) {
tflog.Info(ctx, fmt.Sprintf("Configuring ReadMe client version %s", p.Version))
// Retrieve provider data from configuration
var config readmeProviderModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
// Default values to environment variables, but override with Terraform configuration value if set.
apiToken := os.Getenv("README_API_TOKEN")
apiURL := os.Getenv("README_API_URL")
// Use the config value if it's set
if config.APIToken.ValueString() != "" {
apiToken = config.APIToken.ValueString()
}
if config.APIURL.ValueString() != "" {
apiURL = config.APIURL.ValueString()
}
// Ensure API token is set via the "api_token" attribute or README_API_TOKEN env var.
if apiToken == "" {
resp.Diagnostics.AddAttributeError(
path.Root("api_token"),
"Missing ReadMe API Token.",
"The provider cannot create the Readme API client because there is a missing or empty value for the Readme API token. "+
"Set the token value in the configuration or use the README_API_TOKEN environment variable. "+
"If either is already set, ensure the value is not empty.",
)
}
// API URL is optional, but if it's specified, it must not be set to an empty value.
if !config.APIURL.IsNull() {
apiURL = config.APIURL.ValueString()
if apiURL == "" {
resp.Diagnostics.AddAttributeError(
path.Root("api_url"),
"Missing ReadMe API URL.",
"The provider cannot create the Readme API client because the an API URL is set to an empty value. "+
"Set the correct value in the configuration, use the README_API_URL environment variable, "+
"or leave it unset to use the default value. "+
"If either is already set, ensure the value is not empty.",
)
}
}
if resp.Diagnostics.HasError() {
return
}
ctx = tflog.SetField(ctx, "api_token", apiToken)
ctx = tflog.SetField(ctx, "api_url", apiURL)
ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "api_token")
tflog.Debug(ctx, "Creating ReadMe client")
var client *readme.Client
var err error
if apiURL == "" {
client, err = readme.NewClient(apiToken)
} else {
client, err = readme.NewClient(apiToken, apiURL)
}
// Create a new Readme client using the configuration values
if err != nil {
resp.Diagnostics.AddError(
"Unable to Create Readme API Client.",
"An unexpected error occurred when creating the Readme API client. "+
"If the error is not clear, please contact the provider developers.\n\n"+
"Readme Client Error: "+err.Error(),
)
return
}
// Make the Readme client available during DataSource and Resource type Configure methods.
resp.DataSourceData = client
resp.ResourceData = client
tflog.Info(ctx, "Configured ReadMe client", map[string]any{"success": true})
}
// DataSources defines the data sources implemented in the provider.
func (p *readmeProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewAPIRegistryDataSource,
NewAPISpecificationDataSource,
NewAPISpecificationsDataSource,
NewCategoriesDataSource,
NewCategoryDataSource,
NewCategoryDocsDataSource,
NewChangelogDataSource,
NewCustomPageDataSource,
NewCustomPagesDataSource,
NewDocDataSource,
NewDocSearchDataSource,
NewProjectDataSource,
NewVersionDataSource,
NewVersionsDataSource,
}
}
// Resources defines the resources implemented in the provider.
func (p *readmeProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewAPISpecificationResource,
NewCategoryResource,
NewChangelogResource,
NewCustomPageResource,
NewDocResource,
NewImageResource,
NewVersionResource,
}
}
// boolPoint returns a pointer to a boolean.
func boolPoint(input bool) *bool {
return &input
}
// intPoint returns a pointer to a boolean.
func intPoint(input int) *int {
return &input
}
// apiRequestOptions returns options for making the API request with a version if a version is set.
// Otherwise, it returns an empty `readme.RequestOptions` struct.
func apiRequestOptions(version basetypes.StringValue) readme.RequestOptions {
// If a version is provided, set the request option.
var options readme.RequestOptions
if version.ValueString() != "" {
options = readme.RequestOptions{
Version: version.ValueString(),
}
}
return options
}
// versionClean returns the "clean" version for a version ID.
func versionClean(ctx context.Context, client *readme.Client, versionID string) string {
version, apiResponse, err := client.Version.Get(IDPrefix + versionID)
if err != nil {
tflog.Info(
ctx,
fmt.Sprintf("error resolving version: %s. API response: %+v", err.Error(), apiResponse),
)
return ""
}
if version.VersionClean == "" {
tflog.Info(ctx, "the version returned is empty")
return ""
}
return version.VersionClean
}
// clientError is a helper function for formatting a Terraform diagnostics error response string
// from the client library and API.
//
// It accepts the raw error and APIResponse struct. If the APIResponse includes an error message,
// it will be appended to the error with a line break.
// Functions that make an API request should use the returned string as the second argument to the
// Terraform diagnostics AddError() function, which is used as the detailed message in a Terraform
// error.
func clientError(err error, apiResponse *readme.APIResponse) string {
diagErr := err.Error()
if apiResponse == nil || apiResponse.APIErrorResponse.Message == "" {
return diagErr
}
diagErr = fmt.Sprintf("API Error Message: %s\n", apiResponse.APIErrorResponse.Message)
diagErr += fmt.Sprintf("API Error Response: %+v", apiResponse.APIErrorResponse)
return diagErr
}