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
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ Deployment complete!
Detected changes in 1 resource(s):

Resource: resources.jobs.my_job
email_notifications.on_failure[0]: update
max_concurrent_runs: update
tags['env']: update
email_notifications.on_failure[0]: replace
max_concurrent_runs: replace
tags['env']: remove



=== Configuration changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Deployment complete!
Detected changes in 1 resource(s):

Resource: resources.jobs.my_job
max_concurrent_runs: update
max_concurrent_runs: replace



=== Configuration changes
Expand Down
11 changes: 6 additions & 5 deletions acceptance/bundle/config-remote-sync/job_fields/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ Deployment complete!
Detected changes in 1 resource(s):

Resource: resources.jobs.my_job
email_notifications.no_alert_for_skipped_runs: skip
email_notifications.on_failure: skip
parameters: update
tags['team']: update
trigger.periodic.interval: update
email_notifications.no_alert_for_skipped_runs: add
email_notifications.on_failure: add
parameters: replace
tags['team']: add
trigger.periodic.interval: replace



=== Configuration changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ Deployment complete!
Detected changes in 1 resource(s):

Resource: resources.jobs.my_job
tasks[task_key='task2']: update
tasks[task_key='task3'].depends_on[0].task_key: update
tasks[task_key='task3'].new_cluster.num_workers: update
tasks[task_key='task3'].timeout_seconds: skip
tasks[task_key='task2']: remove
tasks[task_key='task3'].depends_on[0].task_key: replace
tasks[task_key='task3'].new_cluster.num_workers: replace
tasks[task_key='task3'].timeout_seconds: add



=== Configuration changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ Deployment complete!
Detected changes in 2 resource(s):

Resource: resources.jobs.my_job
tasks[task_key='run_pipeline'].pipeline_task.full_refresh: update
tasks[task_key='run_pipeline'].pipeline_task.full_refresh: replace

Resource: resources.pipelines.my_pipeline
development: update
development: replace



=== Configuration changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ Deployment complete!
Detected changes in 2 resource(s):

Resource: resources.jobs.job_one
max_concurrent_runs: update
max_concurrent_runs: replace

Resource: resources.jobs.job_two
max_concurrent_runs: update
max_concurrent_runs: replace



=== Changes in job1.yml
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ Deployment complete!
Detected changes in 2 resource(s):

Resource: resources.jobs.job_one
max_concurrent_runs: update
tags: skip
max_concurrent_runs: replace
tags: add

Resource: resources.jobs.job_two
max_concurrent_runs: update
tags: skip
max_concurrent_runs: replace
tags: add



=== Configuration changes
Expand Down
11 changes: 4 additions & 7 deletions acceptance/bundle/config-remote-sync/output_json/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,12 @@ Deployment complete!
"changes": {
"resources.jobs.test_job": {
"max_concurrent_runs": {
"action": "update",
"old": 1,
"new": 1,
"remote": 3
"operation": "replace",
"value": 3
},
"tags": {
"action": "skip",
"reason": "server_side_default",
"remote": {
"operation": "add",
"value": {
"env": "test"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ Deploying resources...
Updating deployment state...
Deployment complete!

=== Check for changes immediately after deploymentNo changes detected.
=== Check for changes immediately after deployment
No changes detected.


=== Text outputNo changes detected.
=== Text output
No changes detected.


=== JSON output{
=== JSON output
{
"files": null,
"changes": {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ touch dummy.whl
$CLI bundle deploy

title "Check for changes immediately after deployment"
echo
$CLI bundle config-remote-sync

title "Text output"
echo
$CLI bundle config-remote-sync | contains.py "No changes detected"

title "JSON output"
echo
$CLI bundle config-remote-sync -o json > out.json
cat out.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ Deployment complete!
Detected changes in 1 resource(s):

Resource: resources.pipelines.my_pipeline
configuration['key2']: update
notifications[0].alerts: update
notifications[0].email_recipients: update
schema: update
tags['foo']: update
configuration['key2']: add
notifications[0].alerts: replace
notifications[0].email_recipients: replace
schema: replace
tags['foo']: add



=== Configuration changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ Deployment complete!
Detected changes in 1 resource(s):

Resource: resources.jobs.my_job
max_concurrent_runs: update
tags['env']: update
tags['owner']: update
max_concurrent_runs: replace
tags['env']: replace
tags['owner']: add



=== Configuration changes
Expand Down
112 changes: 107 additions & 5 deletions bundle/configsync/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,118 @@ package configsync

import (
"context"
"encoding/json"
"fmt"
"reflect"
"strings"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config/engine"
"github.com/databricks/cli/bundle/deployplan"
"github.com/databricks/cli/bundle/direct"
"github.com/databricks/cli/libs/log"
"github.com/databricks/databricks-sdk-go/marshal"
)

type OperationType string

const (
OperationUnknown OperationType = "unknown"
OperationAdd OperationType = "add"
OperationRemove OperationType = "remove"
OperationReplace OperationType = "replace"
OperationSkip OperationType = "skip"
)

type ConfigChangeDesc struct {
Operation OperationType `json:"operation"`
Value any `json:"value,omitempty"` // Normalized remote value (nil for remove operations)
}

type ResourceChanges map[string]*ConfigChangeDesc

type Changes map[string]ResourceChanges

// normalizeValue converts values to plain Go types suitable for YAML patching
// by using SDK marshaling which properly handles ForceSendFields and other annotations.
func normalizeValue(v any) (any, error) {
Copy link
Contributor Author

@ilyakuz-db ilyakuz-db Jan 30, 2026

Choose a reason for hiding this comment

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

This is moved as is from patch.go

Will check if it's needed and can it be replaced with dyn in following PRs

if v == nil {
return nil, nil
}

switch v.(type) {
case bool, string, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
return v, nil
}

rv := reflect.ValueOf(v)
rt := rv.Type()

if rt.Kind() == reflect.Ptr {
rt = rt.Elem()
}

var data []byte
var err error

if rt.Kind() == reflect.Struct {
data, err = marshal.Marshal(v)
} else {
data, err = json.Marshal(v)
}

if err != nil {
return v, fmt.Errorf("failed to marshal value of type %T: %w", v, err)
}

var normalized any
err = json.Unmarshal(data, &normalized)
if err != nil {
return v, fmt.Errorf("failed to unmarshal value: %w", err)
}

return normalized, nil
}

func convertChangeDesc(path string, cd *deployplan.ChangeDesc) (*ConfigChangeDesc, error) {
hasConfigValue := cd.Old != nil || cd.New != nil

op := OperationUnknown
if shouldSkipField(path, cd) {
return &ConfigChangeDesc{
Operation: OperationSkip,
}, nil
}

if cd.Remote == nil && hasConfigValue {
op = OperationRemove
}
if cd.Remote != nil && hasConfigValue {
op = OperationReplace
}
if cd.Remote != nil && !hasConfigValue {
op = OperationAdd
}

var normalizedValue any
var err error
if op != OperationRemove {
normalizedValue, err = normalizeValue(cd.Remote)
if err != nil {
return nil, fmt.Errorf("failed to normalize remote value: %w", err)
}
}

return &ConfigChangeDesc{
Operation: op,
Value: normalizedValue,
}, nil
}

// DetectChanges compares current remote state with the last deployed state
// and returns a map of resource changes.
func DetectChanges(ctx context.Context, b *bundle.Bundle, engine engine.EngineType) (map[string]deployplan.Changes, error) {
changes := make(map[string]deployplan.Changes)
func DetectChanges(ctx context.Context, b *bundle.Bundle, engine engine.EngineType) (Changes, error) {
changes := make(Changes)

deployBundle := &direct.DeploymentBundle{}
var statePath string
Expand All @@ -31,14 +129,18 @@ func DetectChanges(ctx context.Context, b *bundle.Bundle, engine engine.EngineTy
}

for resourceKey, entry := range plan.Plan {
resourceChanges := make(deployplan.Changes)
resourceChanges := make(ResourceChanges)

if entry.Changes != nil {
for path, changeDesc := range entry.Changes {
if shouldSkipField(path, changeDesc) {
change, err := convertChangeDesc(path, changeDesc)
if err != nil {
return nil, fmt.Errorf("failed to compute config change for path %s: %w", path, err)
}
if change.Operation == OperationSkip {
continue
}
resourceChanges[path] = changeDesc
resourceChanges[path] = change
}
}

Expand Down
10 changes: 5 additions & 5 deletions bundle/configsync/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ import (
"fmt"
"sort"
"strings"

"github.com/databricks/cli/bundle/deployplan"
)

// FormatTextOutput formats the config changes as human-readable text. Useful for debugging
func FormatTextOutput(changes map[string]deployplan.Changes) string {
func FormatTextOutput(changes Changes) string {
var output strings.Builder

if len(changes) == 0 {
Expand All @@ -36,9 +34,11 @@ func FormatTextOutput(changes map[string]deployplan.Changes) string {
sort.Strings(paths)

for _, path := range paths {
changeDesc := resourceChanges[path]
output.WriteString(fmt.Sprintf(" %s: %s\n", path, changeDesc.Action))
configChange := resourceChanges[path]
output.WriteString(fmt.Sprintf(" %s: %s\n", path, configChange.Operation))
}

output.WriteString("\n")
}

return output.String()
Expand Down
5 changes: 2 additions & 3 deletions bundle/configsync/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"path/filepath"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/deployplan"
)

// FileChange represents a change to a bundle configuration file
Expand All @@ -18,8 +17,8 @@ type FileChange struct {

// DiffOutput represents the complete output of the config-remote-sync command
type DiffOutput struct {
Files []FileChange `json:"files"`
Changes map[string]deployplan.Changes `json:"changes"`
Files []FileChange `json:"files"`
Changes Changes `json:"changes"`
}

// SaveFiles writes all file changes to disk.
Expand Down
Loading
Loading