Skip to content

Commit d3427f5

Browse files
authored
Migrate SQL Warehouse to Go SDK (#3044)
* Migrate SQL Warehouse to Go SDK * fix compile * check in diff-schema scrip * fix * fix * test fix * fix * fix * undo extra change * work * work * work * fix * fixes * fix * remove * handle slices as well * fix stubs in exporter test * test * Fix tags diff suppression * fix * Address comments * comments * fix workflow * fix * only diff against merge-base of master * update docs * remove deprecated resources * undo extra docs changes * remove extra space * small tweak * small tweaks * tests and address comments
1 parent 030d446 commit d3427f5

15 files changed

+523
-421
lines changed

common/reflect_resource.go

Lines changed: 93 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,44 @@ func MustSchemaPath(s map[string]*schema.Schema, path ...string) *schema.Schema
7777
// StructToSchema makes schema from a struct type & applies customizations from callback given
7878
func StructToSchema(v any, customize func(map[string]*schema.Schema) map[string]*schema.Schema) map[string]*schema.Schema {
7979
rv := reflect.ValueOf(v)
80-
scm := typeToSchema(rv, rv.Type(), []string{})
80+
scm := typeToSchema(rv, []string{})
8181
if customize != nil {
8282
scm = customize(scm)
8383
}
8484
return scm
8585
}
8686

87+
// SetSuppressDiff adds diff suppression to a schema. This is necessary for non-computed
88+
// fields for which the platform returns a value, but the user has not configured any value.
89+
// For example: the REST API returns `{"tags": {}}` for a resource with no tags.
90+
func SetSuppressDiff(v *schema.Schema) {
91+
v.DiffSuppressFunc = diffSuppressor(fmt.Sprintf("%v", v.Type.Zero()))
92+
}
93+
94+
// SetDefault sets the default value for a schema.
95+
func SetDefault(v *schema.Schema, value any) {
96+
v.Default = value
97+
v.Optional = true
98+
v.Required = false
99+
}
100+
101+
// SetReadOnly sets the schema to be read-only (i.e. computed, non-optional).
102+
// This should be used for fields that are not user-configurable but are returned
103+
// by the platform.
104+
func SetReadOnly(v *schema.Schema) {
105+
v.Optional = false
106+
v.Required = false
107+
v.MaxItems = 0
108+
v.Computed = true
109+
}
110+
111+
// SetRequired sets the schema to be required.
112+
func SetRequired(v *schema.Schema) {
113+
v.Optional = false
114+
v.Required = true
115+
v.Computed = false
116+
}
117+
87118
func isOptional(typeField reflect.StructField) bool {
88119
if strings.Contains(typeField.Tag.Get("json"), "omitempty") {
89120
return true
@@ -175,26 +206,52 @@ func diffSuppressor(zero string) func(k, old, new string, d *schema.ResourceData
175206
log.Printf("[DEBUG] Suppressing diff for %v: platform=%#v config=%#v", k, old, new)
176207
return true
177208
}
209+
if strings.HasSuffix(k, ".#") && new == "0" && old != "0" {
210+
field := strings.TrimSuffix(k, ".#")
211+
log.Printf("[DEBUG] Suppressing diff for list or set %v: no value configured but platform returned some value (likely {})", field)
212+
return true
213+
}
178214
return false
179215
}
180216
}
181217

218+
type field struct {
219+
sf reflect.StructField
220+
v reflect.Value
221+
}
222+
223+
func listAllFields(v reflect.Value) []field {
224+
t := v.Type()
225+
fields := make([]field, 0, v.NumField())
226+
for i := 0; i < v.NumField(); i++ {
227+
f := t.Field(i)
228+
if f.Anonymous {
229+
fields = append(fields, listAllFields(v.Field(i))...)
230+
} else {
231+
fields = append(fields, field{
232+
sf: f,
233+
v: v.Field(i),
234+
})
235+
}
236+
}
237+
return fields
238+
}
239+
182240
// typeToSchema converts struct into terraform schema. `path` is used for block suppressions
183241
// special path element `"0"` is used to denote either arrays or sets of elements
184-
func typeToSchema(v reflect.Value, t reflect.Type, path []string) map[string]*schema.Schema {
242+
func typeToSchema(v reflect.Value, path []string) map[string]*schema.Schema {
185243
scm := map[string]*schema.Schema{}
186244
rk := v.Kind()
187245
if rk == reflect.Ptr {
188246
v = v.Elem()
189-
t = v.Type()
190247
rk = v.Kind()
191248
}
192249
if rk != reflect.Struct {
193250
panic(fmt.Errorf("Schema value of Struct is expected, but got %s: %#v", reflectKind(rk), v))
194251
}
195-
for i := 0; i < v.NumField(); i++ {
196-
typeField := t.Field(i)
197-
252+
fields := listAllFields(v)
253+
for _, field := range fields {
254+
typeField := field.sf
198255
tfTag := typeField.Tag.Get("tf")
199256

200257
fieldName := chooseFieldName(typeField)
@@ -260,7 +317,7 @@ func typeToSchema(v reflect.Value, t reflect.Type, path []string) map[string]*sc
260317
scm[fieldName].Type = schema.TypeList
261318
elem := typeField.Type.Elem()
262319
sv := reflect.New(elem).Elem()
263-
nestedSchema := typeToSchema(sv, elem, append(path, fieldName, "0"))
320+
nestedSchema := typeToSchema(sv, append(path, fieldName, "0"))
264321
if strings.Contains(tfTag, "suppress_diff") {
265322
blockCount := strings.Join(append(path, fieldName, "#"), ".")
266323
scm[fieldName].DiffSuppressFunc = makeEmptyBlockSuppressFunc(blockCount)
@@ -279,7 +336,7 @@ func typeToSchema(v reflect.Value, t reflect.Type, path []string) map[string]*sc
279336
elem := typeField.Type // changed from ptr
280337
sv := reflect.New(elem) // changed from ptr
281338

282-
nestedSchema := typeToSchema(sv, elem, append(path, fieldName, "0"))
339+
nestedSchema := typeToSchema(sv, append(path, fieldName, "0"))
283340
if strings.Contains(tfTag, "suppress_diff") {
284341
blockCount := strings.Join(append(path, fieldName, "#"), ".")
285342
scm[fieldName].DiffSuppressFunc = makeEmptyBlockSuppressFunc(blockCount)
@@ -310,7 +367,7 @@ func typeToSchema(v reflect.Value, t reflect.Type, path []string) map[string]*sc
310367
case reflect.Struct:
311368
sv := reflect.New(elem).Elem()
312369
scm[fieldName].Elem = &schema.Resource{
313-
Schema: typeToSchema(sv, elem, append(path, fieldName, "0")),
370+
Schema: typeToSchema(sv, append(path, fieldName, "0")),
314371
}
315372
}
316373
default:
@@ -341,6 +398,20 @@ func IsRequestEmpty(v any) (bool, error) {
341398
return !isNotEmpty, err
342399
}
343400

401+
// isGoSdk returns true if the struct is from databricks-sdk-go or embeds a struct from databricks-sdk-go.
402+
func isGoSdk(v reflect.Value) bool {
403+
if strings.Contains(v.Type().PkgPath(), "databricks-sdk-go") {
404+
return true
405+
}
406+
for i := 0; i < v.NumField(); i++ {
407+
f := v.Type().Field(i)
408+
if f.Anonymous && isGoSdk(v.Field(i)) {
409+
return true
410+
}
411+
}
412+
return false
413+
}
414+
344415
func iterFields(rv reflect.Value, path []string, s map[string]*schema.Schema,
345416
cb func(fieldSchema *schema.Schema, path []string, valueField *reflect.Value) error) error {
346417
rk := rv.Kind()
@@ -350,9 +421,10 @@ func iterFields(rv reflect.Value, path []string, s map[string]*schema.Schema,
350421
if !rv.IsValid() {
351422
return fmt.Errorf("%s: got invalid reflect value %#v", path, rv)
352423
}
353-
isGoSDK := strings.Contains(rv.Type().PkgPath(), "databricks-sdk-go")
354-
for i := 0; i < rv.NumField(); i++ {
355-
typeField := rv.Type().Field(i)
424+
isGoSDK := isGoSdk(rv)
425+
fields := listAllFields(rv)
426+
for _, field := range fields {
427+
typeField := field.sf
356428
fieldName := chooseFieldName(typeField)
357429
if fieldName == "-" {
358430
continue
@@ -370,7 +442,7 @@ func iterFields(rv reflect.Value, path []string, s map[string]*schema.Schema,
370442
if fieldSchema.Optional && defaultEmpty && !omitEmpty {
371443
return fmt.Errorf("inconsistency: %s is optional, default is empty, but has no omitempty", fieldName)
372444
}
373-
valueField := rv.Field(i)
445+
valueField := field.v
374446
err := cb(fieldSchema, append(path, fieldName), &valueField)
375447
if err != nil {
376448
return fmt.Errorf("%s: %s", fieldName, err)
@@ -393,13 +465,15 @@ func collectionToMaps(v any, s *schema.Schema) ([]any, error) {
393465
return nil, fmt.Errorf("not resource")
394466
}
395467
var allItems []reflect.Value
396-
if s.MaxItems == 1 {
397-
allItems = append(allItems, reflect.ValueOf(v))
398-
} else {
399-
vs := reflect.ValueOf(v)
400-
for i := 0; i < vs.Len(); i++ {
401-
allItems = append(allItems, vs.Index(i))
468+
rv := reflect.ValueOf(v)
469+
rvType := rv.Type().Kind()
470+
isList := rvType == reflect.Array || rvType == reflect.Slice
471+
if isList {
472+
for i := 0; i < rv.Len(); i++ {
473+
allItems = append(allItems, rv.Index(i))
402474
}
475+
} else {
476+
allItems = append(allItems, rv)
403477
}
404478
for _, v := range allItems {
405479
data := map[string]any{}

common/reflect_resource_test.go

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"reflect"
77
"testing"
88

9+
"github.com/databricks/databricks-sdk-go/service/sql"
910
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1011
"github.com/stretchr/testify/assert"
1112
)
@@ -444,7 +445,7 @@ func TestTypeToSchemaNoStruct(t *testing.T) {
444445
fmt.Sprintf("%s", p))
445446
}()
446447
v := reflect.ValueOf(1)
447-
typeToSchema(v, v.Type(), []string{})
448+
typeToSchema(v, []string{})
448449
}
449450

450451
func TestTypeToSchemaUnsupported(t *testing.T) {
@@ -457,7 +458,7 @@ func TestTypeToSchemaUnsupported(t *testing.T) {
457458
New chan int `json:"new"`
458459
}
459460
v := reflect.ValueOf(nonsense{})
460-
typeToSchema(v, v.Type(), []string{})
461+
typeToSchema(v, []string{})
461462
}
462463

463464
type data map[string]any
@@ -634,3 +635,79 @@ func TestDataResourceWithID(t *testing.T) {
634635
assert.Equal(t, "out: id", d.Get("out"))
635636
assert.Equal(t, "abc", d.Id())
636637
}
638+
639+
func TestStructToSchema_go_sdk_embedded(t *testing.T) {
640+
type T struct {
641+
sql.GetWarehouseResponse
642+
Extra string `json:"extra,omitempty" tf:"computed"`
643+
}
644+
s := StructToSchema(T{}, nil)
645+
autoStopMins, ok := s["auto_stop_mins"]
646+
assert.True(t, ok)
647+
assert.Equal(t, schema.TypeInt, autoStopMins.Type)
648+
assert.True(t, autoStopMins.Optional)
649+
}
650+
651+
func TestStructToData_go_sdk_embedded(t *testing.T) {
652+
type T struct {
653+
sql.GetWarehouseResponse
654+
Extra string `json:"extra,omitempty" tf:"computed"`
655+
}
656+
s := StructToSchema(T{}, nil)
657+
SetRequired(s["cluster_size"])
658+
r := &schema.Resource{
659+
Schema: s,
660+
}
661+
d := r.TestResourceData()
662+
d.Set("cluster_size", "original")
663+
err := StructToData(T{
664+
GetWarehouseResponse: sql.GetWarehouseResponse{
665+
ClusterSize: "abc123",
666+
},
667+
Extra: "extra",
668+
}, s, d)
669+
assert.NoError(t, err)
670+
assert.Equal(t, "abc123", d.Get("cluster_size"))
671+
assert.Equal(t, "extra", d.Get("extra"))
672+
}
673+
674+
func TestStructToSchema_go_sdk_field(t *testing.T) {
675+
type T struct {
676+
Warehouse *sql.GetWarehouseResponse `json:"warehouse,omitempty"`
677+
Extra string `json:"extra,omitempty" tf:"computed"`
678+
}
679+
s := StructToSchema(T{}, nil)
680+
warehouse, ok := s["warehouse"]
681+
assert.True(t, ok)
682+
autoStopMins, ok := warehouse.Elem.(*schema.Resource).Schema["auto_stop_mins"]
683+
assert.True(t, ok)
684+
assert.Equal(t, schema.TypeInt, autoStopMins.Type)
685+
assert.True(t, autoStopMins.Optional)
686+
}
687+
688+
func TestStructToData_go_sdk_field(t *testing.T) {
689+
type T struct {
690+
Warehouse *sql.GetWarehouseResponse `json:"warehouse,omitempty"`
691+
Extra string `json:"extra,omitempty" tf:"computed"`
692+
}
693+
s := StructToSchema(T{}, nil)
694+
SetRequired(MustSchemaPath(s, "warehouse", "cluster_size"))
695+
r := &schema.Resource{
696+
Schema: s,
697+
}
698+
d := r.TestResourceData()
699+
d.Set("warehouse", []map[string]any{
700+
{
701+
"cluster_size": "original",
702+
},
703+
})
704+
err := StructToData(T{
705+
Warehouse: &sql.GetWarehouseResponse{
706+
ClusterSize: "abc123",
707+
},
708+
Extra: "extra",
709+
}, s, d)
710+
assert.NoError(t, err)
711+
assert.Equal(t, "abc123", d.Get("warehouse.0.cluster_size"))
712+
assert.Equal(t, "extra", d.Get("extra"))
713+
}

0 commit comments

Comments
 (0)