Skip to content

Commit 6b833b7

Browse files
authored
direct: Add RemapState resource method (#3586)
## Changes New RemapState() method on resource that transform remote state to config type. ## Why To be used to detect remote drift. ## Tests Unit tests.
1 parent 436ded2 commit 6b833b7

File tree

7 files changed

+155
-6
lines changed

7 files changed

+155
-6
lines changed

bundle/terranova/tnresources/adapter.go

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,18 @@ type IResource interface {
2626
// Example: func (*ResourceJob) PrepareState(input *resources.Job) *jobs.JobSettings
2727
PrepareState(input any) any
2828

29+
// [Required if type(remoteState) != type(state)] RemapState adapts remote state to local state type.
30+
// The adapted remote state will then be compared with newState to detect remote drift.
31+
// Adaptation is not necessary (but possible) if types already match.
32+
// Example: func (*ResourceJob) RemapState(jobs *jobs.Job) *jobs.JobSettings
33+
RemapState(input any) any
34+
2935
// DoRefresh reads and returns remote state from the backend. The return type defines schema for remote field resolution.
30-
// Example: func (r *ResourceJob) DoRefresh(ctx context.Context, id string) (*jobs.Job, error) {
36+
// Example: func (r *ResourceJob) DoRefresh(ctx context.Context, id string) (*jobs.Job, error)
3137
DoRefresh(ctx context.Context, id string) (remoteState any, e error)
3238

3339
// DoDelete deletes the resource.
34-
// Example: func (r *ResourceJob) DoDelete(ctx context.Context, id string) error {
40+
// Example: func (r *ResourceJob) DoDelete(ctx context.Context, id string) error
3541
DoDelete(ctx context.Context, id string) error
3642

3743
// [Optional] FieldTriggers returns actions to trigger when given fields are changed.
@@ -49,11 +55,11 @@ type IResourceNoRefresh interface {
4955
// We pass newState as a pointer but it is never nil. Changes to it will be persisted in the state, so should be used carefully.
5056

5157
// DoCreate creates a new resource from the newState.
52-
// Example: func (r *ResourceJob) DoCreate(ctx context.Context, newState *jobs.JobSettings) (string, error) {
58+
// Example: func (r *ResourceJob) DoCreate(ctx context.Context, newState *jobs.JobSettings) (string, error)
5359
DoCreate(ctx context.Context, newState any) (id string, e error)
5460

5561
// DoUpdate updates the resource. ID must not change as a result of this operation.
56-
// Example: func (r *ResourceJob) DoUpdate(ctx context.Context, id string, newState *jobs.JobSettings) error {
62+
// Example: func (r *ResourceJob) DoUpdate(ctx context.Context, id string, newState *jobs.JobSettings) error
5763
DoUpdate(ctx context.Context, id string, newState any) error
5864

5965
// [Optional] DoUpdateWithID performs an update that may result in resource having a new ID
@@ -73,11 +79,11 @@ type IResourceNoRefresh interface {
7379
// Note, resource implementations don't pick between IResourceNoRefresh and IResourceWithRefresh, they can make independent decision for each of the methods.
7480
type IResourceWithRefresh interface {
7581
// DoCreate creates a new resource from the newState. Returns id of the resource and remote state.
76-
// Example: func (r *ResourceVolume) DoCreate(ctx context.Context, newState *catalog.CreateWarehouseRequestContent) (string, *catalog.VolumeInfo, error) {
82+
// Example: func (r *ResourceVolume) DoCreate(ctx context.Context, newState *catalog.CreateWarehouseRequestContent) (string, *catalog.VolumeInfo, error)
7783
DoCreate(ctx context.Context, newState any) (id string, remoteState any, e error)
7884

7985
// DoUpdate updates the resource. ID must not change as a result of this operation. Returns remote state.
80-
// Example: func (r *ResourceSchema) DoUpdate(ctx context.Context, id string, newState *catalog.CreateSchema) (*catalog.SchemaInfo, error) {
86+
// Example: func (r *ResourceSchema) DoUpdate(ctx context.Context, id string, newState *catalog.CreateSchema) (*catalog.SchemaInfo, error)
8187
DoUpdate(ctx context.Context, id string, newState any) (remoteState any, e error)
8288

8389
// Optional: updates that may change ID. Returns new id and remote state when available.
@@ -95,6 +101,7 @@ type IResourceWithRefresh interface {
95101
type Adapter struct {
96102
// Required:
97103
prepareState *calladapt.BoundCaller
104+
remapState *calladapt.BoundCaller
98105
doRefresh *calladapt.BoundCaller
99106
doDelete *calladapt.BoundCaller
100107
doCreate *calladapt.BoundCaller
@@ -123,6 +130,7 @@ func NewAdapter(typedNil any, client *databricks.WorkspaceClient) (*Adapter, err
123130
impl := outs[0]
124131
adapter := &Adapter{
125132
prepareState: nil,
133+
remapState: nil,
126134
doRefresh: nil,
127135
doDelete: nil,
128136
doCreate: nil,
@@ -174,6 +182,12 @@ func (a *Adapter) initMethods(resource any) error {
174182
return err
175183
}
176184

185+
// RemapState is optional when remote type already matches state type.
186+
a.remapState, err = calladapt.PrepareCall(resource, calladapt.TypeOf[IResource](), "RemapState")
187+
if err != nil {
188+
return err
189+
}
190+
177191
a.doRefresh, err = prepareCallRequired(resource, "DoRefresh")
178192
if err != nil {
179193
return err
@@ -251,6 +265,17 @@ func (a *Adapter) validate() error {
251265
"DoUpdate newState", a.doUpdate.InTypes[2], stateType,
252266
}
253267

268+
// If RemapState is implemented, validate its signature.
269+
// Otherwise require remote type to equal state type so remapping isn't needed.
270+
if a.remapState != nil {
271+
validations = append(validations,
272+
"RemapState input", a.remapState.InTypes[0], remoteType,
273+
"RemapState return", a.remapState.OutTypes[0], stateType,
274+
)
275+
} else if remoteType != stateType {
276+
return fmt.Errorf("RemapState method not found and remote type %v must match state type %v", remoteType, stateType)
277+
}
278+
254279
// Check if this is WithRefresh version (returns 3 values: id, remoteState, error)
255280
if len(a.doCreate.OutTypes) == 3 {
256281
validations = append(validations, "DoCreate remoteState return", a.doCreate.OutTypes[1], remoteType)
@@ -323,6 +348,18 @@ func (a *Adapter) PrepareState(input any) (any, error) {
323348
return outs[0], nil
324349
}
325350

351+
func (a *Adapter) RemapState(remoteState any) (any, error) {
352+
if a.remapState == nil {
353+
return remoteState, nil
354+
}
355+
356+
outs, err := a.remapState.Call(remoteState)
357+
if err != nil {
358+
return nil, err
359+
}
360+
return outs[0], nil
361+
}
362+
326363
func (a *Adapter) DoRefresh(ctx context.Context, id string) (any, error) {
327364
outs, err := a.doRefresh.Call(ctx, id)
328365
if err != nil {

bundle/terranova/tnresources/all_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ import (
88

99
"github.com/databricks/cli/bundle/config/resources"
1010
"github.com/databricks/cli/bundle/deployplan"
11+
"github.com/databricks/cli/libs/dyn"
12+
"github.com/databricks/cli/libs/structs/structaccess"
1113
"github.com/databricks/cli/libs/structs/structpath"
1214
"github.com/databricks/cli/libs/structs/structwalk"
1315
"github.com/databricks/cli/libs/testserver"
1416
"github.com/databricks/databricks-sdk-go"
1517
"github.com/databricks/databricks-sdk-go/service/apps"
1618
"github.com/databricks/databricks-sdk-go/service/catalog"
1719
"github.com/databricks/databricks-sdk-go/service/database"
20+
"github.com/stretchr/testify/assert"
1821
"github.com/stretchr/testify/require"
1922
)
2023

@@ -147,6 +150,31 @@ func testCRUD(t *testing.T, group string, adapter *Adapter, client *databricks.W
147150
require.Equal(t, remote, remoteStateFromWaitUpdate)
148151
}
149152

153+
remappedState, err := adapter.RemapState(remote)
154+
require.NoError(t, err)
155+
require.NotNil(t, remappedState)
156+
157+
require.NoError(t, structwalk.Walk(newState, func(path *structpath.PathNode, val any) {
158+
remoteValue, err := structaccess.Get(remappedState, dyn.MustPathFromString(path.DynPath()))
159+
if err != nil {
160+
t.Errorf("Failed to read %s from remapped remote state %#v", path.DynPath(), remappedState)
161+
}
162+
if val == nil {
163+
// t.Logf("Ignoring %s nil, remoteValue=%#v", path.String(), remoteValue)
164+
return
165+
}
166+
v := reflect.ValueOf(val)
167+
if v.IsZero() {
168+
// t.Logf("Ignoring %s zero (%#v), remoteValue=%#v", path.String(), val, remoteValue)
169+
// testserver can set field to backend-generated value
170+
return
171+
}
172+
// t.Logf("Testing %s v=%#v, remoteValue=%#v", path.String(), val, remoteValue)
173+
// We expect fields set explicitly to be preserved by testserver, which is true for all resources as of today.
174+
// If not true for your resource, add exception here:
175+
assert.Equal(t, val, remoteValue, path.DynPath())
176+
}))
177+
150178
err = adapter.DoDelete(ctx, createdID)
151179
require.NoError(t, err)
152180

bundle/terranova/tnresources/job.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ func (*ResourceJob) PrepareState(input *resources.Job) *jobs.JobSettings {
2424
return &input.JobSettings
2525
}
2626

27+
func (*ResourceJob) RemapState(jobs *jobs.Job) *jobs.JobSettings {
28+
return jobs.Settings
29+
}
30+
2731
func (r *ResourceJob) DoRefresh(ctx context.Context, id string) (*jobs.Job, error) {
2832
idInt, err := parseJobID(id)
2933
if err != nil {

bundle/terranova/tnresources/pipeline.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,44 @@ func (*ResourcePipeline) PrepareState(input *resources.Pipeline) *pipelines.Crea
2323
return &input.CreatePipeline
2424
}
2525

26+
func (*ResourcePipeline) RemapState(p *pipelines.GetPipelineResponse) *pipelines.CreatePipeline {
27+
spec := p.Spec
28+
return &pipelines.CreatePipeline{
29+
// TODO: Fields that are not available in GetPipelineResponse (like AllowDuplicateNames) should be added to resource's ignore_remote_changes list so that they never produce a call to action
30+
AllowDuplicateNames: false,
31+
BudgetPolicyId: spec.BudgetPolicyId,
32+
Catalog: spec.Catalog,
33+
Channel: spec.Channel,
34+
Clusters: spec.Clusters,
35+
Configuration: spec.Configuration,
36+
Continuous: spec.Continuous,
37+
Deployment: spec.Deployment,
38+
Development: spec.Development,
39+
DryRun: false,
40+
Edition: spec.Edition,
41+
Environment: spec.Environment,
42+
EventLog: spec.EventLog,
43+
Filters: spec.Filters,
44+
GatewayDefinition: spec.GatewayDefinition,
45+
Id: spec.Id,
46+
IngestionDefinition: spec.IngestionDefinition,
47+
Libraries: spec.Libraries,
48+
Name: spec.Name,
49+
Notifications: spec.Notifications,
50+
Photon: spec.Photon,
51+
RestartWindow: spec.RestartWindow,
52+
RootPath: spec.RootPath,
53+
RunAs: p.RunAs,
54+
Schema: spec.Schema,
55+
Serverless: spec.Serverless,
56+
Storage: spec.Storage,
57+
Tags: spec.Tags,
58+
Target: spec.Target,
59+
Trigger: spec.Trigger,
60+
ForceSendFields: filterFields[pipelines.CreatePipeline](spec.ForceSendFields, "AllowDuplicateNames", "DryRun", "RunAs"),
61+
}
62+
}
63+
2664
func (r *ResourcePipeline) DoRefresh(ctx context.Context, id string) (*pipelines.GetPipelineResponse, error) {
2765
return r.client.Pipelines.GetByPipelineId(ctx, id)
2866
}

bundle/terranova/tnresources/schema.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ func (*ResourceSchema) PrepareState(input *resources.Schema) *catalog.CreateSche
2222
return &input.CreateSchema
2323
}
2424

25+
func (*ResourceSchema) RemapState(info *catalog.SchemaInfo) *catalog.CreateSchema {
26+
return &catalog.CreateSchema{
27+
CatalogName: info.CatalogName,
28+
Comment: info.Comment,
29+
Name: info.Name,
30+
Properties: info.Properties,
31+
StorageRoot: info.StorageRoot,
32+
ForceSendFields: filterFields[catalog.CreateSchema](info.ForceSendFields),
33+
}
34+
}
35+
2536
func (r *ResourceSchema) DoRefresh(ctx context.Context, id string) (*catalog.SchemaInfo, error) {
2637
return r.client.Schemas.GetByFullName(ctx, id)
2738
}

bundle/terranova/tnresources/sql_warehouse.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,25 @@ func (*ResourceSqlWarehouse) PrepareState(input *resources.SqlWarehouse) *sql.Cr
2323
return &input.CreateWarehouseRequest
2424
}
2525

26+
func (*ResourceSqlWarehouse) RemapState(warehouse *sql.GetWarehouseResponse) *sql.CreateWarehouseRequest {
27+
return &sql.CreateWarehouseRequest{
28+
AutoStopMins: warehouse.AutoStopMins,
29+
Channel: warehouse.Channel,
30+
ClusterSize: warehouse.ClusterSize,
31+
CreatorName: warehouse.CreatorName,
32+
EnablePhoton: warehouse.EnablePhoton,
33+
EnableServerlessCompute: warehouse.EnableServerlessCompute,
34+
InstanceProfileArn: warehouse.InstanceProfileArn,
35+
MaxNumClusters: warehouse.MaxNumClusters,
36+
MinNumClusters: warehouse.MinNumClusters,
37+
Name: warehouse.Name,
38+
SpotInstancePolicy: warehouse.SpotInstancePolicy,
39+
Tags: warehouse.Tags,
40+
WarehouseType: sql.CreateWarehouseRequestWarehouseType(warehouse.WarehouseType),
41+
ForceSendFields: filterFields[sql.CreateWarehouseRequest](warehouse.ForceSendFields),
42+
}
43+
}
44+
2645
// DoRefresh reads the warehouse by id.
2746
func (r *ResourceSqlWarehouse) DoRefresh(ctx context.Context, id string) (*sql.GetWarehouseResponse, error) {
2847
return r.client.Warehouses.GetById(ctx, id)

bundle/terranova/tnresources/volume.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ func (*ResourceVolume) PrepareState(input *resources.Volume) *catalog.CreateVolu
2424
return &input.CreateVolumeRequestContent
2525
}
2626

27+
func (*ResourceVolume) RemapState(info *catalog.VolumeInfo) *catalog.CreateVolumeRequestContent {
28+
return &catalog.CreateVolumeRequestContent{
29+
CatalogName: info.CatalogName,
30+
Comment: info.Comment,
31+
Name: info.Name,
32+
SchemaName: info.SchemaName,
33+
StorageLocation: info.StorageLocation,
34+
VolumeType: info.VolumeType,
35+
ForceSendFields: filterFields[catalog.CreateVolumeRequestContent](info.ForceSendFields),
36+
}
37+
}
38+
2739
func (r *ResourceVolume) DoRefresh(ctx context.Context, id string) (*catalog.VolumeInfo, error) {
2840
return r.client.Volumes.ReadByName(ctx, id)
2941
}

0 commit comments

Comments
 (0)