-
Notifications
You must be signed in to change notification settings - Fork 200
Expand file tree
/
Copy pathdefaults.go
More file actions
243 lines (211 loc) · 9.44 KB
/
Copy pathdefaults.go
File metadata and controls
243 lines (211 loc) · 9.44 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
package configsync
import (
"reflect"
"strings"
)
type (
skipAlways struct{}
skipIfZeroOrNil struct{}
skipIfEmptyOrDefault struct {
defaults map[string]any
}
// skipBackendDefault skips a field, regardless of its remote value, when it is
// absent from config. Used for fields the backend or a cluster policy may fill
// in: their remote-only value must not be synced into config. Fields the user
// does manage (present in config) still sync normally.
skipBackendDefault struct{}
)
var (
alwaysSkip = skipAlways{}
zeroOrNil = skipIfZeroOrNil{}
emptyEmailNotifications = skipIfEmptyOrDefault{defaults: map[string]any{"no_alert_for_skipped_runs": false}}
backendDefault = skipBackendDefault{}
)
// serverSideDefaults contains all hardcoded server-side defaults.
// This is a temporary solution until the bundle plan issue is resolved.
// Fields mapped to alwaysSkip are always considered defaults regardless of value.
// Other fields are compared using reflect.DeepEqual.
var serverSideDefaults = map[string]any{
// Job-level fields
"resources.jobs.*.timeout_seconds": zeroOrNil,
"resources.jobs.*.email_notifications": emptyEmailNotifications,
"resources.jobs.*.webhook_notifications": map[string]any{},
"resources.jobs.*.edit_mode": alwaysSkip, // set by CLI
"resources.jobs.*.performance_target": "PERFORMANCE_OPTIMIZED",
// Task-level fields
"resources.jobs.*.tasks[*].run_if": "ALL_SUCCESS",
"resources.jobs.*.tasks[*].disabled": false,
"resources.jobs.*.tasks[*].timeout_seconds": zeroOrNil,
"resources.jobs.*.tasks[*].notebook_task.source": "WORKSPACE",
"resources.jobs.*.tasks[*].email_notifications": emptyEmailNotifications,
"resources.jobs.*.tasks[*].webhook_notifications": map[string]any{},
"resources.jobs.*.tasks[*].pipeline_task.full_refresh": false,
"resources.jobs.*.tasks[*].for_each_task.task.run_if": "ALL_SUCCESS",
"resources.jobs.*.tasks[*].for_each_task.task.disabled": false,
"resources.jobs.*.tasks[*].for_each_task.task.timeout_seconds": zeroOrNil,
"resources.jobs.*.tasks[*].for_each_task.task.notebook_task.source": "WORKSPACE",
"resources.jobs.*.tasks[*].for_each_task.task.email_notifications": emptyEmailNotifications,
"resources.jobs.*.tasks[*].for_each_task.task.webhook_notifications": map[string]any{},
// Cluster fields (tasks)
"resources.jobs.*.tasks[*].new_cluster.aws_attributes": alwaysSkip,
"resources.jobs.*.tasks[*].new_cluster.azure_attributes": alwaysSkip,
"resources.jobs.*.tasks[*].new_cluster.gcp_attributes": alwaysSkip,
"resources.jobs.*.tasks[*].new_cluster.data_security_mode": "SINGLE_USER", // TODO this field is computed on some workspaces in integration tests, check why and if we can skip it
"resources.jobs.*.tasks[*].new_cluster.enable_elastic_disk": alwaysSkip, // deprecated field
"resources.jobs.*.tasks[*].new_cluster.single_user_name": alwaysSkip,
// custom_tags and cluster_log_conf are commonly injected by cluster policies
// when the user omits them, so they exist only remotely. Syncing them back leaks
// one environment's policy values into (often shared) config and breaks deploys in
// other environments. TODO: move to backend_defaults in resources.yml once
// configsync filtering is migrated to the direct engine lifecycle metadata.
"resources.jobs.*.tasks[*].new_cluster.custom_tags": backendDefault,
"resources.jobs.*.tasks[*].new_cluster.cluster_log_conf": backendDefault,
// Cluster fields (job_clusters)
"resources.jobs.*.job_clusters[*].new_cluster.aws_attributes": alwaysSkip,
"resources.jobs.*.job_clusters[*].new_cluster.azure_attributes": alwaysSkip,
"resources.jobs.*.job_clusters[*].new_cluster.gcp_attributes": alwaysSkip,
"resources.jobs.*.job_clusters[*].new_cluster.data_security_mode": "SINGLE_USER", // TODO this field is computed on some workspaces in integration tests, check why and if we can skip it
"resources.jobs.*.job_clusters[*].new_cluster.enable_elastic_disk": alwaysSkip, // deprecated field
"resources.jobs.*.job_clusters[*].new_cluster.single_user_name": alwaysSkip,
"resources.jobs.*.job_clusters[*].new_cluster.custom_tags": backendDefault, // see tasks[*].new_cluster.custom_tags
"resources.jobs.*.job_clusters[*].new_cluster.cluster_log_conf": backendDefault, // see tasks[*].new_cluster.cluster_log_conf
// Standalone cluster fields
"resources.clusters.*.aws_attributes": alwaysSkip,
"resources.clusters.*.azure_attributes": alwaysSkip,
"resources.clusters.*.gcp_attributes": alwaysSkip,
"resources.clusters.*.data_security_mode": "SINGLE_USER",
"resources.clusters.*.driver_node_type_id": alwaysSkip,
"resources.clusters.*.enable_elastic_disk": alwaysSkip,
"resources.clusters.*.single_user_name": alwaysSkip,
"resources.clusters.*.custom_tags": backendDefault, // see jobs.*.tasks[*].new_cluster.custom_tags
"resources.clusters.*.cluster_log_conf": backendDefault, // see jobs.*.tasks[*].new_cluster.cluster_log_conf
// Experiment fields
"resources.experiments.*.artifact_location": alwaysSkip,
// Registered model fields
"resources.registered_models.*.full_name": alwaysSkip,
"resources.registered_models.*.metastore_id": alwaysSkip,
"resources.registered_models.*.owner": alwaysSkip,
"resources.registered_models.*.storage_location": alwaysSkip,
// Volume fields
"resources.volumes.*.storage_location": alwaysSkip,
// SQL warehouse fields
"resources.sql_warehouses.*.creator_name": alwaysSkip,
"resources.sql_warehouses.*.min_num_clusters": int64(1),
"resources.sql_warehouses.*.warehouse_type": "CLASSIC",
// Terraform defaults
"resources.jobs.*.run_as": alwaysSkip,
// Pipeline fields
"resources.pipelines.*.storage": alwaysSkip,
"resources.pipelines.*.continuous": false,
// Dashboard fields
// etag is output-only and never present in config. The plan re-promotes etag
// drift to Update via ResourceDashboard.OverrideChangeDesc (needed for deploy's
// modified-remotely detection), so configsync cannot rely on the plan's Skip
// action and must exclude the field explicitly.
"resources.dashboards.*.etag": alwaysSkip,
}
// shouldSkipField checks if a field should be skipped in change detection.
// When hasConfigValue is true (field is set in config or saved state), only
// "always skip" fields are skipped. Backend defaults are only skipped when the
// field is not in config/state, matching the behavior of shouldSkipBackendDefault
// in the direct deployment engine.
func shouldSkipField(path string, value any, hasConfigValue bool) bool {
for pattern, expected := range serverSideDefaults {
if matchPattern(pattern, path) {
if _, ok := expected.(skipAlways); ok {
return true
}
if hasConfigValue {
return false
}
if _, ok := expected.(skipBackendDefault); ok {
return true
}
if _, ok := expected.(skipIfZeroOrNil); ok {
return value == nil || value == int64(0)
}
if marker, ok := expected.(skipIfEmptyOrDefault); ok {
m, ok := value.(map[string]any)
if !ok {
return false
}
if len(m) == 0 {
return true
}
return reflect.DeepEqual(m, marker.defaults)
}
return reflect.DeepEqual(value, expected)
}
}
return false
}
func matchPattern(pattern, path string) bool {
patternParts := strings.Split(pattern, ".")
pathParts := strings.Split(path, ".")
return matchParts(patternParts, pathParts)
}
func matchParts(patternParts, pathParts []string) bool {
if len(patternParts) == 0 && len(pathParts) == 0 {
return true
}
if len(patternParts) == 0 || len(pathParts) == 0 {
return false
}
patternPart := patternParts[0]
pathPart := pathParts[0]
if patternPart == "*" {
return matchParts(patternParts[1:], pathParts[1:])
}
if strings.Contains(patternPart, "[*]") {
prefix := strings.Split(patternPart, "[*]")[0]
if strings.HasPrefix(pathPart, prefix) && strings.Contains(pathPart, "[") {
return matchParts(patternParts[1:], pathParts[1:])
}
return false
}
if patternPart == pathPart {
return matchParts(patternParts[1:], pathParts[1:])
}
return false
}
// resetValues contains all values that should be used to reset CLI-defaulted fields.
// If CLI-defaulted field is changed on remote and should be disabled (e.g. queueing disabled -> remote field is nil)
// we can't define it in the config as "null" because CLI default will be applied again.
var resetValues = map[string]any{
"resources.jobs.*.queue": map[string]any{
"enabled": false,
},
}
func resetValueIfNeeded(path string, value any) any {
for pattern, expected := range resetValues {
if matchPattern(pattern, path) {
return expected
}
}
return value
}
// prefixedNameFields lists resource name field patterns where the name prefix
// (e.g. "[dev user] ") is applied during deployment and should be stripped
// when syncing remote changes back to config.
var prefixedNameFields = []string{
"resources.jobs.*.name",
"resources.pipelines.*.name",
"resources.dashboards.*.display_name",
}
// stripNamePrefix strips the configured name prefix from name field values
// so that the raw (unprefixed) name is written back to the config YAML.
func stripNamePrefix(path string, value any, prefix string) any {
if prefix == "" {
return value
}
s, ok := value.(string)
if !ok {
return value
}
for _, pattern := range prefixedNameFields {
if matchPattern(pattern, path) {
return strings.TrimPrefix(s, prefix)
}
}
return value
}