Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 43 additions & 6 deletions bundle/terranova/tnresources/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@ type IResource interface {
// Example: func (*ResourceJob) PrepareState(input *resources.Job) *jobs.JobSettings
PrepareState(input any) any

// [Required if type(remoteState) != type(state)] RemapState adapts remote state to local state type.
// The adapted remote state will then be compared with newState to detect remote drift.
// Adaptation is not necessary (but possible) if types already match.
// Example: func (*ResourceJob) RemapState(jobs *jobs.Job) *jobs.JobSettings
RemapState(input any) any

// DoRefresh reads and returns remote state from the backend. The return type defines schema for remote field resolution.
// Example: func (r *ResourceJob) DoRefresh(ctx context.Context, id string) (*jobs.Job, error) {
// Example: func (r *ResourceJob) DoRefresh(ctx context.Context, id string) (*jobs.Job, error)
DoRefresh(ctx context.Context, id string) (remoteState any, e error)

// DoDelete deletes the resource.
// Example: func (r *ResourceJob) DoDelete(ctx context.Context, id string) error {
// Example: func (r *ResourceJob) DoDelete(ctx context.Context, id string) error
DoDelete(ctx context.Context, id string) error

// [Optional] FieldTriggers returns actions to trigger when given fields are changed.
Expand All @@ -49,11 +55,11 @@ type IResourceNoRefresh interface {
// 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.

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

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

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

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

// Optional: updates that may change ID. Returns new id and remote state when available.
Expand All @@ -95,6 +101,7 @@ type IResourceWithRefresh interface {
type Adapter struct {
// Required:
prepareState *calladapt.BoundCaller
remapState *calladapt.BoundCaller
doRefresh *calladapt.BoundCaller
doDelete *calladapt.BoundCaller
doCreate *calladapt.BoundCaller
Expand Down Expand Up @@ -123,6 +130,7 @@ func NewAdapter(typedNil any, client *databricks.WorkspaceClient) (*Adapter, err
impl := outs[0]
adapter := &Adapter{
prepareState: nil,
remapState: nil,
doRefresh: nil,
doDelete: nil,
doCreate: nil,
Expand Down Expand Up @@ -174,6 +182,12 @@ func (a *Adapter) initMethods(resource any) error {
return err
}

// RemapState is optional when remote type already matches state type.
a.remapState, err = calladapt.PrepareCall(resource, calladapt.TypeOf[IResource](), "RemapState")
if err != nil {
return err
}

a.doRefresh, err = prepareCallRequired(resource, "DoRefresh")
if err != nil {
return err
Expand Down Expand Up @@ -251,6 +265,17 @@ func (a *Adapter) validate() error {
"DoUpdate newState", a.doUpdate.InTypes[2], stateType,
}

// If RemapState is implemented, validate its signature.
// Otherwise require remote type to equal state type so remapping isn't needed.
if a.remapState != nil {
validations = append(validations,
"RemapState input", a.remapState.InTypes[0], remoteType,
"RemapState return", a.remapState.OutTypes[0], stateType,
)
} else if remoteType != stateType {
return fmt.Errorf("RemapState method not found and remote type %v must match state type %v", remoteType, stateType)
}

// Check if this is WithRefresh version (returns 3 values: id, remoteState, error)
if len(a.doCreate.OutTypes) == 3 {
validations = append(validations, "DoCreate remoteState return", a.doCreate.OutTypes[1], remoteType)
Expand Down Expand Up @@ -323,6 +348,18 @@ func (a *Adapter) PrepareState(input any) (any, error) {
return outs[0], nil
}

func (a *Adapter) RemapState(remoteState any) (any, error) {
if a.remapState == nil {
return remoteState, nil
}

outs, err := a.remapState.Call(remoteState)
if err != nil {
return nil, err
}
return outs[0], nil
}

func (a *Adapter) DoRefresh(ctx context.Context, id string) (any, error) {
outs, err := a.doRefresh.Call(ctx, id)
if err != nil {
Expand Down
28 changes: 28 additions & 0 deletions bundle/terranova/tnresources/all_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ import (

"github.com/databricks/cli/bundle/config/resources"
"github.com/databricks/cli/bundle/deployplan"
"github.com/databricks/cli/libs/dyn"
"github.com/databricks/cli/libs/structs/structaccess"
"github.com/databricks/cli/libs/structs/structpath"
"github.com/databricks/cli/libs/structs/structwalk"
"github.com/databricks/cli/libs/testserver"
"github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/service/apps"
"github.com/databricks/databricks-sdk-go/service/catalog"
"github.com/databricks/databricks-sdk-go/service/database"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

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

remappedState, err := adapter.RemapState(remote)
require.NoError(t, err)
require.NotNil(t, remappedState)

require.NoError(t, structwalk.Walk(newState, func(path *structpath.PathNode, val any) {
remoteValue, err := structaccess.Get(remappedState, dyn.MustPathFromString(path.DynPath()))
if err != nil {
t.Errorf("Failed to read %s from remapped remote state %#v", path.DynPath(), remappedState)
}
if val == nil {
// t.Logf("Ignoring %s nil, remoteValue=%#v", path.String(), remoteValue)
return
}
v := reflect.ValueOf(val)
if v.IsZero() {
// t.Logf("Ignoring %s zero (%#v), remoteValue=%#v", path.String(), val, remoteValue)
// testserver can set field to backend-generated value
return
}
// t.Logf("Testing %s v=%#v, remoteValue=%#v", path.String(), val, remoteValue)
// We expect fields set explicitly to be preserved by testserver, which is true for all resources as of today.
// If not true for your resource, add exception here:
assert.Equal(t, val, remoteValue, path.DynPath())
}))

err = adapter.DoDelete(ctx, createdID)
require.NoError(t, err)

Expand Down
4 changes: 4 additions & 0 deletions bundle/terranova/tnresources/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ func (*ResourceJob) PrepareState(input *resources.Job) *jobs.JobSettings {
return &input.JobSettings
}

func (*ResourceJob) RemapState(jobs *jobs.Job) *jobs.JobSettings {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be worth adding type aliases for readability. We have 3 types that we're dealing with here:

  • Config type: resources.Job
  • State type: jobs.JobSettings
  • Remote type: jobs.Job

If we alias each of these to e.g. JobConfig, JobState, JobRemote, then it is unambiguous in which direction the remapping happens. It's not clear without context that this function maps from the remote type to the state type.

return jobs.Settings
}

func (r *ResourceJob) DoRefresh(ctx context.Context, id string) (*jobs.Job, error) {
idInt, err := parseJobID(id)
if err != nil {
Expand Down
38 changes: 38 additions & 0 deletions bundle/terranova/tnresources/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,44 @@ func (*ResourcePipeline) PrepareState(input *resources.Pipeline) *pipelines.Crea
return &input.CreatePipeline
}

func (*ResourcePipeline) RemapState(p *pipelines.GetPipelineResponse) *pipelines.CreatePipeline {
spec := p.Spec
return &pipelines.CreatePipeline{
// 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
AllowDuplicateNames: false,
BudgetPolicyId: spec.BudgetPolicyId,
Catalog: spec.Catalog,
Channel: spec.Channel,
Clusters: spec.Clusters,
Configuration: spec.Configuration,
Continuous: spec.Continuous,
Deployment: spec.Deployment,
Development: spec.Development,
DryRun: false,
Edition: spec.Edition,
Environment: spec.Environment,
EventLog: spec.EventLog,
Filters: spec.Filters,
GatewayDefinition: spec.GatewayDefinition,
Id: spec.Id,
IngestionDefinition: spec.IngestionDefinition,
Libraries: spec.Libraries,
Name: spec.Name,
Notifications: spec.Notifications,
Photon: spec.Photon,
RestartWindow: spec.RestartWindow,
RootPath: spec.RootPath,
RunAs: p.RunAs,
Schema: spec.Schema,
Serverless: spec.Serverless,
Storage: spec.Storage,
Tags: spec.Tags,
Target: spec.Target,
Trigger: spec.Trigger,
ForceSendFields: filterFields[pipelines.CreatePipeline](spec.ForceSendFields, "AllowDuplicateNames", "DryRun", "RunAs"),
}
}

func (r *ResourcePipeline) DoRefresh(ctx context.Context, id string) (*pipelines.GetPipelineResponse, error) {
return r.client.Pipelines.GetByPipelineId(ctx, id)
}
Expand Down
11 changes: 11 additions & 0 deletions bundle/terranova/tnresources/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ func (*ResourceSchema) PrepareState(input *resources.Schema) *catalog.CreateSche
return &input.CreateSchema
}

func (*ResourceSchema) RemapState(info *catalog.SchemaInfo) *catalog.CreateSchema {
return &catalog.CreateSchema{
CatalogName: info.CatalogName,
Comment: info.Comment,
Name: info.Name,
Properties: info.Properties,
StorageRoot: info.StorageRoot,
ForceSendFields: filterFields[catalog.CreateSchema](info.ForceSendFields),
}
}

func (r *ResourceSchema) DoRefresh(ctx context.Context, id string) (*catalog.SchemaInfo, error) {
return r.client.Schemas.GetByFullName(ctx, id)
}
Expand Down
19 changes: 19 additions & 0 deletions bundle/terranova/tnresources/sql_warehouse.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ func (*ResourceSqlWarehouse) PrepareState(input *resources.SqlWarehouse) *sql.Cr
return &input.CreateWarehouseRequest
}

func (*ResourceSqlWarehouse) RemapState(warehouse *sql.GetWarehouseResponse) *sql.CreateWarehouseRequest {
return &sql.CreateWarehouseRequest{
AutoStopMins: warehouse.AutoStopMins,
Channel: warehouse.Channel,
ClusterSize: warehouse.ClusterSize,
CreatorName: warehouse.CreatorName,
EnablePhoton: warehouse.EnablePhoton,
EnableServerlessCompute: warehouse.EnableServerlessCompute,
InstanceProfileArn: warehouse.InstanceProfileArn,
MaxNumClusters: warehouse.MaxNumClusters,
MinNumClusters: warehouse.MinNumClusters,
Name: warehouse.Name,
SpotInstancePolicy: warehouse.SpotInstancePolicy,
Tags: warehouse.Tags,
WarehouseType: sql.CreateWarehouseRequestWarehouseType(warehouse.WarehouseType),
ForceSendFields: filterFields[sql.CreateWarehouseRequest](warehouse.ForceSendFields),
}
}

// DoRefresh reads the warehouse by id.
func (r *ResourceSqlWarehouse) DoRefresh(ctx context.Context, id string) (*sql.GetWarehouseResponse, error) {
return r.client.Warehouses.GetById(ctx, id)
Expand Down
12 changes: 12 additions & 0 deletions bundle/terranova/tnresources/volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ func (*ResourceVolume) PrepareState(input *resources.Volume) *catalog.CreateVolu
return &input.CreateVolumeRequestContent
}

func (*ResourceVolume) RemapState(info *catalog.VolumeInfo) *catalog.CreateVolumeRequestContent {
return &catalog.CreateVolumeRequestContent{
CatalogName: info.CatalogName,
Comment: info.Comment,
Name: info.Name,
SchemaName: info.SchemaName,
StorageLocation: info.StorageLocation,
VolumeType: info.VolumeType,
ForceSendFields: filterFields[catalog.CreateVolumeRequestContent](info.ForceSendFields),
}
}

func (r *ResourceVolume) DoRefresh(ctx context.Context, id string) (*catalog.VolumeInfo, error) {
return r.client.Volumes.ReadByName(ctx, id)
}
Expand Down
Loading