Skip to content

Commit 9888803

Browse files
tanmay-dbrauchy
andauthored
[Internal] Refactor data source methods for SDKv2 resources (#5150)
## Changes <!-- Summary of your changes that are easy to understand --> These changes are done upstream since these shouldn't be part of autogeneration as they are used by legacy SDKv2 resources ## Tests <!-- How is this tested? Please see the checklist below and also describe any other relevant tests --> Existing tests NO_CHANGELOG=true --------- Co-authored-by: Omer Lachish <[email protected]>
1 parent 7a055a1 commit 9888803

File tree

2 files changed

+251
-243
lines changed

2 files changed

+251
-243
lines changed

common/datasource.go

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package common
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"reflect"
7+
8+
"github.com/databricks/databricks-sdk-go"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
)
11+
12+
// Deprecated: migrate to WorkspaceData
13+
func DataResource(sc any, read func(context.Context, any, *DatabricksClient) error) Resource {
14+
// TODO: migrate to go1.18 and get schema from second function argument?..
15+
s := StructToSchema(sc, func(m map[string]*schema.Schema) map[string]*schema.Schema {
16+
return m
17+
})
18+
return Resource{
19+
Schema: s,
20+
Read: func(ctx context.Context, d *schema.ResourceData, m *DatabricksClient) (err error) {
21+
ptr := reflect.New(reflect.ValueOf(sc).Type())
22+
DataToReflectValue(d, s, ptr.Elem())
23+
err = read(ctx, ptr.Interface(), m)
24+
if err != nil {
25+
err = nicerError(ctx, err, "read data")
26+
}
27+
StructToData(ptr.Elem().Interface(), s, d)
28+
// check if the resource schema has the `id` attribute (marked with `json:"id"` in the provided structure).
29+
// and if yes, then use it as resource ID. If not, then use default value for resource ID (`_`)
30+
if _, ok := s["id"]; ok {
31+
d.SetId(d.Get("id").(string))
32+
} else {
33+
d.SetId("_")
34+
}
35+
return
36+
},
37+
}
38+
}
39+
40+
// WorkspaceData is a generic way to define workspace data resources in Terraform provider.
41+
//
42+
// Example usage:
43+
//
44+
// type catalogsData struct {
45+
// Ids []string `json:"ids,omitempty" tf:"computed,slice_set"`
46+
// }
47+
// return common.WorkspaceData(func(ctx context.Context, data *catalogsData, w *databricks.WorkspaceClient) error {
48+
// catalogs, err := w.Catalogs.ListAll(ctx)
49+
// ...
50+
// })
51+
func WorkspaceData[T any](read func(context.Context, *T, *databricks.WorkspaceClient) error) Resource {
52+
return genericDatabricksData((*DatabricksClient).WorkspaceClient, func(ctx context.Context, s struct{}, t *T, wc *databricks.WorkspaceClient) error {
53+
return read(ctx, t, wc)
54+
}, false, NoCustomize)
55+
}
56+
57+
// WorkspaceDataWithParams defines a data source that can be used to read data from the workspace API.
58+
// It differs from WorkspaceData in that it separates the definition of the computed fields (the resource type)
59+
// from the definition of the user-supplied parameters.
60+
//
61+
// The first type parameter is the type representing parameters that a user may provide to the data source. These
62+
// are the attributes that the user can specify in the data source configuration, but are not part of the resource
63+
// type. If there are no extra attributes, this should be `struct{}`. If there are any fields with the same JSON
64+
// name as fields in the resource type, these fields will override the values from the resource type.
65+
//
66+
// The second type parameter is the type of the resource. This can be a type directly from the SDK, or a custom
67+
// type defined in the provider that embeds the SDK type.
68+
//
69+
// The single argument is a function that will be called to read the data from the workspace API, returning the
70+
// value of the resource type. The function should return an error if the data cannot be read or the resource cannot
71+
// be found.
72+
//
73+
// Example usage:
74+
//
75+
// type SqlWarehouse struct { ... }
76+
//
77+
// type SqlWarehouseDataParams struct {
78+
// Id string `json:"id" tf:"computed,optional"`
79+
// Name string `json:"name" tf:"computed,optional"`
80+
// }
81+
//
82+
// WorkspaceDataWithParams(
83+
// func(ctx context.Context, data SqlWarehouseDataParams, w *databricks.WorkspaceClient) (*SqlWarehouse, error) {
84+
// // User-provided attributes are present in the `data` parameter.
85+
// // The resource should be returned.
86+
// ...
87+
// })
88+
func WorkspaceDataWithParams[T, P any](read func(context.Context, P, *databricks.WorkspaceClient) (*T, error)) Resource {
89+
return genericDatabricksData((*DatabricksClient).WorkspaceClient, func(ctx context.Context, o P, s *T, w *databricks.WorkspaceClient) error {
90+
res, err := read(ctx, o, w)
91+
if err != nil {
92+
return err
93+
}
94+
*s = *res
95+
return nil
96+
}, true, NoCustomize)
97+
}
98+
99+
// WorkspaceDataWithCustomizeFunc defines a data source that can be used to read data from the workspace API.
100+
// It differs from WorkspaceData in that it allows the schema to be customized further using a
101+
// customizeSchemaFunc function.
102+
//
103+
// The additional argument is a function that will be called to customize the schema of the data source.
104+
105+
func WorkspaceDataWithCustomizeFunc[T any](
106+
read func(context.Context, *T, *databricks.WorkspaceClient) error,
107+
customizeSchemaFunc func(map[string]*schema.Schema) map[string]*schema.Schema) Resource {
108+
return genericDatabricksData((*DatabricksClient).WorkspaceClient, func(ctx context.Context, s struct{}, t *T, wc *databricks.WorkspaceClient) error {
109+
return read(ctx, t, wc)
110+
}, false, customizeSchemaFunc)
111+
}
112+
113+
// AccountData is a generic way to define account data resources in Terraform provider.
114+
//
115+
// Example usage:
116+
//
117+
// type metastoresData struct {
118+
// Ids map[string]string `json:"ids,omitempty" tf:"computed"`
119+
// }
120+
// return common.AccountData(func(ctx context.Context, d *metastoresData, acc *databricks.AccountClient) error {
121+
// metastores, err := acc.Metastores.List(ctx)
122+
// ...
123+
// })
124+
func AccountData[T any](read func(context.Context, *T, *databricks.AccountClient) error) Resource {
125+
return genericDatabricksData((*DatabricksClient).AccountClient, func(ctx context.Context, s struct{}, t *T, ac *databricks.AccountClient) error {
126+
return read(ctx, t, ac)
127+
}, false, NoCustomize)
128+
}
129+
130+
// AccountDataWithParams defines a data source that can be used to read data from the account API.
131+
// It differs from AccountData in that it allows extra attributes to be provided as a separate argument,
132+
// so the original type used to define the resource can also be used to define the data source.
133+
//
134+
// The first type parameter is the type of the resource. This can be a type directly from the SDK, or a custom
135+
// type defined in the provider that embeds the SDK type.
136+
//
137+
// The second type parameter is the type of the extra attributes that should be provided to the data source. These
138+
// are the attributes that the user can specify in the data source configuration, but are not part of the resource
139+
// type. If there are no extra attributes, this should be `struct{}`. If there are any fields with the same JSON
140+
// name as fields in the resource type, these fields will override the values from the resource type.
141+
//
142+
// The single argument is a function that will be called to read the data from the workspace API, returning the
143+
// requested resource. The function should return an error if the data cannot be read or the resource cannot be
144+
// found.
145+
//
146+
// Example usage:
147+
//
148+
// type MwsWorkspace struct { ... }
149+
//
150+
// type MwsWorkspaceDataParams struct {
151+
// Id string `json:"id" tf:"computed,optional"`
152+
// Name string `json:"name" tf:"computed,optional"`
153+
// }
154+
//
155+
// AccountDataWithParams(
156+
// func(ctx context.Context, data MwsWorkspaceDataParams, a *databricks.AccountClient) (*MwsWorkspace, error) {
157+
// // User-provided attributes are present in the `data` parameter.
158+
// // The resource should be populated in the `workspace` parameter.
159+
// ...
160+
// })
161+
func AccountDataWithParams[T, P any](read func(context.Context, P, *databricks.AccountClient) (*T, error)) Resource {
162+
return genericDatabricksData((*DatabricksClient).AccountClient, func(ctx context.Context, o P, s *T, a *databricks.AccountClient) error {
163+
res, err := read(ctx, o, a)
164+
if err != nil {
165+
return err
166+
}
167+
*s = *res
168+
return nil
169+
}, true, NoCustomize)
170+
}
171+
172+
// genericDatabricksData is generic and common way to define both account and workspace data and calls their respective clients.
173+
//
174+
// If hasOther is true, all of the fields of SdkType will be marked as computed in the final schema, and the fields
175+
// from OtherFields will be overlaid on top of the schema generated by SdkType. Otherwise, the schema generated by
176+
// SdkType will be used directly.
177+
func genericDatabricksData[T, P, C any](
178+
getClient func(*DatabricksClient) (C, error),
179+
read func(context.Context, P, *T, C) error,
180+
hasOther bool,
181+
customizeSchemaFunc func(map[string]*schema.Schema) map[string]*schema.Schema) Resource {
182+
var dummy T
183+
var other P
184+
otherFields := StructToSchema(other, nil)
185+
186+
s := StructToSchema(dummy, nil)
187+
// For WorkspaceData and AccountData, a single data type is used to represent all of the fields of
188+
// the resource, so its configuration is correct. For the *WithParams methods, the SdkType parameter
189+
// is copied directly from the resource definition, which means that all fields from that type are
190+
// computed and optional, and the fields from OtherFields are overlaid on top of the schema generated
191+
// by SdkType.
192+
if hasOther {
193+
for k := range s {
194+
s[k].Computed = true
195+
s[k].Required = false
196+
s[k].Optional = true
197+
}
198+
for k, v := range otherFields {
199+
s[k] = v
200+
}
201+
}
202+
// `id` attribute must be marked as computed, otherwise it's not set!
203+
if v, ok := s["id"]; ok {
204+
v.Computed = true
205+
v.Required = false
206+
}
207+
// allow c
208+
s = customizeSchemaFunc(s)
209+
210+
return Resource{
211+
Schema: s,
212+
Read: func(ctx context.Context, d *schema.ResourceData, client *DatabricksClient) (err error) {
213+
defer func() {
214+
// using recoverable() would cause more complex rewrapping of DataToStructPointer & StructToData
215+
if panic := recover(); panic != nil {
216+
err = fmt.Errorf("panic: %v", panic)
217+
}
218+
}()
219+
var dummy T
220+
var other P
221+
DataToStructPointer(d, s, &other)
222+
DataToStructPointer(d, s, &dummy)
223+
c, err := getClient(client)
224+
if err != nil {
225+
return nicerError(ctx, err, "get client")
226+
}
227+
err = read(ctx, other, &dummy, c)
228+
if err != nil {
229+
err = nicerError(ctx, err, "read data")
230+
}
231+
StructToData(&dummy, s, d)
232+
// check if the resource schema has the `id` attribute (marked with `json:"id"` in the provided structure).
233+
// and if yes, then use it as resource ID. If not, then use default value for resource ID (`_`)
234+
if _, ok := s["id"]; ok {
235+
d.SetId(d.Get("id").(string))
236+
} else {
237+
d.SetId("_")
238+
}
239+
return
240+
},
241+
}
242+
}
243+
244+
// NoClientData is a generic way to define data resources in Terraform provider that doesn't require any client.
245+
// usage is similar to AccountData and WorkspaceData, but the read function doesn't take a client.
246+
func NoClientData[T any](read func(context.Context, *T) error) Resource {
247+
return genericDatabricksData(func(*DatabricksClient) (any, error) { return nil, nil },
248+
func(ctx context.Context, s struct{}, t *T, ac any) error {
249+
return read(ctx, t)
250+
}, false, NoCustomize)
251+
}

0 commit comments

Comments
 (0)