-
Notifications
You must be signed in to change notification settings - Fork 160
Expand file tree
/
Copy pathbundle.go
More file actions
371 lines (311 loc) · 12 KB
/
bundle.go
File metadata and controls
371 lines (311 loc) · 12 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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
// Package bundle is the top level package for Declarative Automation Bundles.
//
// A bundle is represented by the [Bundle] type. It consists of configuration
// and runtime state, such as a client to a Databricks workspace.
// Every mutation to a bundle's configuration or state is represented as a [Mutator].
// This interface makes every mutation observable and lets us reason about sequencing.
package bundle
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"time"
"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/bundle/direct"
"github.com/databricks/cli/bundle/env"
"github.com/databricks/cli/bundle/metadata"
"github.com/databricks/cli/libs/auth"
"github.com/databricks/cli/libs/cache"
"github.com/databricks/cli/libs/fileset"
"github.com/databricks/cli/libs/locker"
"github.com/databricks/cli/libs/log"
"github.com/databricks/cli/libs/logdiag"
libsync "github.com/databricks/cli/libs/sync"
"github.com/databricks/cli/libs/tags"
"github.com/databricks/cli/libs/telemetry/protos"
"github.com/databricks/cli/libs/vfs"
"github.com/databricks/databricks-sdk-go"
"github.com/google/uuid"
"github.com/hashicorp/terraform-exec/tfexec"
)
const internalFolder = ".internal"
// Filename where resources are stored for DATABRICKS_BUNDLE_ENGINE=direct
const resourcesFilename = "resources.json"
// Filename where resources are stored for DATABRICKS_BUNDLE_ENGINE=terraform
const terraformStateFilename = "terraform.tfstate"
// Filename where config snapshot is stored for experimental YAML sync
const configSnapshotFilename = "resources-config-sync-snapshot.json"
// This struct is used as a communication channel to collect metrics
// from all over the bundle codebase to finally be emitted as telemetry.
type Metrics struct {
ConfigurationFileCount int64
TargetCount int64
DeploymentId uuid.UUID
BoolValues []protos.BoolMapEntry
PythonAddedResourcesCount int64
PythonUpdatedResourcesCount int64
ExecutionTimes []protos.IntMapEntry
LocalCacheMeasurementsMs []protos.IntMapEntry // Local cache measurements stored as milliseconds
DeployPlanMetrics []protos.IntMapEntry // Deployment plan and graph metrics (direct engine)
}
// SetBoolValue sets the value of a boolean metric.
// If the metric does not exist, it is created.
// If the metric exists, it is updated.
// Ensures that the metric is unique
func (m *Metrics) SetBoolValue(key string, value bool) {
for i, v := range m.BoolValues {
if v.Key == key {
m.BoolValues[i].Value = value
return
}
}
m.BoolValues = append(m.BoolValues, protos.BoolMapEntry{Key: key, Value: value})
}
func (m *Metrics) AddBoolValue(key string, value bool) {
m.BoolValues = append(m.BoolValues, protos.BoolMapEntry{Key: key, Value: value})
}
// AddDurationValue sets the value of a duration metric in milliseconds.
// The value is added to the list of measurements.
func (m *Metrics) AddDurationValue(key string, value time.Duration) {
valueMs := value.Milliseconds()
m.LocalCacheMeasurementsMs = append(m.LocalCacheMeasurementsMs, protos.IntMapEntry{Key: key, Value: valueMs})
}
type Bundle struct {
// BundleRootPath is the local path to the root directory of the bundle.
// It is set when we instantiate a new bundle instance.
BundleRootPath string
// BundleRoot is a virtual filesystem path to [BundleRootPath].
// Exclusively use this field for filesystem operations.
BundleRoot vfs.Path
// SyncRootPath is the local path to the root directory of files that are synchronized to the workspace.
// By default, it is the same as [BundleRootPath].
// If it is different, it must be an ancestor to [BundleRootPath].
// That is, [SyncRootPath] must contain [BundleRootPath].
SyncRootPath string
// SyncRoot is a virtual filesystem path to [SyncRootPath].
// Exclusively use this field for filesystem operations.
SyncRoot vfs.Path
// Path to the root of git worktree containing the bundle.
// https://git-scm.com/docs/git-worktree
WorktreeRoot vfs.Path
// Config contains the bundle configuration.
// It is loaded from the bundle configuration files and mutators may update it.
Config config.Root
// Target stores a snapshot of the Root.Bundle.Target configuration when it was selected by SelectTarget.
Target *config.Target `json:"target_config,omitempty" bundle:"internal"`
// Metadata about the bundle deployment. This is the interface Databricks services
// rely on to integrate with bundles when they need additional information about
// a bundle deployment.
//
// After deploy, a file containing the metadata (metadata.json) can be found
// in the WSFS location containing the bundle state.
Metadata metadata.Metadata
// Store a pointer to the workspace client.
// It can be initialized on demand after loading the configuration.
clientOnce sync.Once
client *databricks.WorkspaceClient
clientErr error
// Files that are synced to the workspace.file_path
Files []fileset.File
// Stores an initialized copy of this bundle's Terraform wrapper.
Terraform *tfexec.Terraform
// Stores the locker responsible for acquiring/releasing a deployment lock.
Locker *locker.Locker
// TerraformPlanPath is the path to the plan from the terraform CLI
TerraformPlanPath string
// If true, the plan is empty and applying it will not do anything
TerraformPlanIsEmpty bool
// (direct only) deployment implementation and state
DeploymentBundle direct.DeploymentBundle
// if true, we skip approval checks for deploy, destroy resources and delete
// files
AutoApprove bool
// SkipLocalFileValidation makes path translation tolerant of missing local files.
// When set, TranslatePaths computes workspace paths without verifying files exist.
// Used by config-remote-sync: a user may modify resource paths remotely (e.g.,
// rename a pipeline root folder in the UI), and the updated paths may not exist
// locally. Path translation is still needed to produce fully resolved paths for
// comparison with remote state, but local file validation would incorrectly fail.
SkipLocalFileValidation bool
// Tagging is used to normalize tag keys and values.
// The implementation depends on the cloud being targeted.
Tagging tags.Cloud
// Cache is used for caching API responses (e.g., current user).
// By default, caching is enabled. Set DATABRICKS_CACHE_ENABLED=false to disable caching.
Cache *cache.Cache
Metrics Metrics
}
func Load(ctx context.Context, path string) (*Bundle, error) {
b := &Bundle{
BundleRootPath: filepath.Clean(path),
BundleRoot: vfs.MustNew(path),
}
configFile, err := config.FileNames.FindInPath(path)
if err != nil {
return nil, err
}
log.Debugf(ctx, "Found bundle root at %s (file %s)", b.BundleRootPath, configFile)
return b, nil
}
// MustLoad returns a bundle configuration.
// The errors are recorded by logdiag, check with logdiag.HasError().
func MustLoad(ctx context.Context) *Bundle {
root, err := mustGetRoot(ctx)
if err != nil {
logdiag.LogError(ctx, err)
return nil
}
logdiag.SetRoot(ctx, root)
b, err := Load(ctx, root)
if err != nil {
logdiag.LogError(ctx, err)
return nil
}
return b
}
// TryLoad returns a bundle configuration if there is one, but doesn't fail if there isn't one.
// The errors are recorded by logdiag, check with logdiag.HasError().
// It returns a `nil` bundle if a bundle was not found.
func TryLoad(ctx context.Context) *Bundle {
root, err := tryGetRoot(ctx)
if err != nil {
logdiag.LogError(ctx, err)
return nil
}
// No root is fine in this function.
if root == "" {
return nil
}
logdiag.SetRoot(ctx, root)
b, err := Load(ctx, root)
if err != nil {
logdiag.LogError(ctx, err)
return nil
}
return b
}
func (b *Bundle) WorkspaceClientE() (*databricks.WorkspaceClient, error) {
b.clientOnce.Do(func() {
var err error
b.client, err = b.Config.Workspace.Client()
if err != nil {
b.clientErr = fmt.Errorf("cannot resolve bundle auth configuration: %w", err)
}
})
return b.client, b.clientErr
}
func (b *Bundle) WorkspaceClient() *databricks.WorkspaceClient {
client, err := b.WorkspaceClientE()
if err != nil {
panic(err)
}
return client
}
// SetWorkpaceClient sets the workspace client for this bundle.
// This is used to inject a mock client for testing.
func (b *Bundle) SetWorkpaceClient(w *databricks.WorkspaceClient) {
b.clientOnce.Do(func() {})
b.client = w
}
// ClearWorkspaceClient resets the workspace client cache, allowing
// WorkspaceClientE() to attempt client creation again on the next call.
func (b *Bundle) ClearWorkspaceClient() {
b.clientOnce = sync.Once{}
b.client = nil
b.clientErr = nil
}
// LocalStateDir returns directory to use for temporary files for this bundle without creating
// Scoped to the bundle's target.
func (b *Bundle) GetLocalStateDir(ctx context.Context, paths ...string) string {
if b.Config.Bundle.Target == "" {
panic("target not set")
}
cacheDirName, exists := env.TempDir(ctx)
if !exists || cacheDirName == "" {
cacheDirName = filepath.Join(
// Anchor at bundle root directory.
b.BundleRootPath,
// Static cache directory.
".databricks",
"bundle",
)
}
// Fixed components of the result path.
parts := []string{
cacheDirName,
// Scope with target name.
b.Config.Bundle.Target,
}
// Append dynamic components of the result path.
parts = append(parts, paths...)
// Make directory if it doesn't exist yet.
dir := filepath.Join(parts...)
return dir
}
// LocalStateDir returns directory to use for temporary files for this bundle.
// Directory is created and initialized with .gitignore
// Scoped to the bundle's target.
func (b *Bundle) LocalStateDir(ctx context.Context, paths ...string) (string, error) {
dir := b.GetLocalStateDir(ctx, paths...)
err := os.MkdirAll(dir, 0o700)
if err != nil {
return "", err
}
libsync.WriteGitIgnore(ctx, b.BundleRootPath)
return dir, nil
}
// This directory is used to store and automaticaly sync internal bundle files, such as, f.e
// notebook trampoline files for Python wheel and etc.
func (b *Bundle) InternalDir(ctx context.Context) (string, error) {
cacheDir, err := b.LocalStateDir(ctx)
if err != nil {
return "", err
}
dir := filepath.Join(cacheDir, internalFolder)
err = os.MkdirAll(dir, 0o700)
if err != nil {
return dir, err
}
return dir, nil
}
// GetSyncIncludePatterns returns a list of user defined includes
// And also adds InternalDir folder to include list for sync command
// so this folder is always synced
func (b *Bundle) GetSyncIncludePatterns(ctx context.Context) ([]string, error) {
internalDir, err := b.InternalDir(ctx)
if err != nil {
return nil, err
}
internalDirRel, err := filepath.Rel(b.BundleRootPath, internalDir)
if err != nil {
return nil, err
}
return append(b.Config.Sync.Include, filepath.ToSlash(filepath.Join(internalDirRel, "*.*"))), nil
}
// AuthEnv returns a map with environment variables and their values
// derived from the workspace client configuration that was resolved
// in the context of this bundle.
//
// This map can be used to configure authentication for tools that
// we call into from this bundle context.
func (b *Bundle) AuthEnv() (map[string]string, error) {
if b.client == nil {
return nil, errors.New("workspace client not initialized yet")
}
cfg := b.client.Config
return auth.Env(cfg), nil
}
// StateFilenameDirect returns (relative remote path, relative local path) for direct engine resource state
func (b *Bundle) StateFilenameDirect(ctx context.Context) (string, string) {
return resourcesFilename, filepath.ToSlash(filepath.Join(b.GetLocalStateDir(ctx), resourcesFilename))
}
func (b *Bundle) StateFilenameTerraform(ctx context.Context) (string, string) {
return terraformStateFilename, filepath.ToSlash(filepath.Join(b.GetLocalStateDir(ctx), "terraform", terraformStateFilename))
}
// StateFilenameConfigSnapshot returns (relative remote path, relative local path) for config snapshot state
func (b *Bundle) StateFilenameConfigSnapshot(ctx context.Context) (string, string) {
return configSnapshotFilename, filepath.ToSlash(filepath.Join(b.GetLocalStateDir(ctx), configSnapshotFilename))
}