2023-06-26 23:31:20 +00:00
|
|
|
// Package bundle is the top level package for Databricks Asset Bundles.
|
2022-12-12 11:49:25 +00:00
|
|
|
//
|
|
|
|
// 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.
|
2022-11-18 09:57:31 +00:00
|
|
|
package bundle
|
|
|
|
|
|
|
|
import (
|
2023-08-11 12:28:05 +00:00
|
|
|
"context"
|
2023-03-29 14:36:35 +00:00
|
|
|
"fmt"
|
2022-11-30 13:40:41 +00:00
|
|
|
"os"
|
2022-11-18 09:57:31 +00:00
|
|
|
"path/filepath"
|
2022-11-23 14:20:03 +00:00
|
|
|
"sync"
|
2022-11-18 09:57:31 +00:00
|
|
|
|
2023-05-16 16:35:39 +00:00
|
|
|
"github.com/databricks/cli/bundle/config"
|
|
|
|
"github.com/databricks/cli/folders"
|
|
|
|
"github.com/databricks/cli/libs/git"
|
|
|
|
"github.com/databricks/cli/libs/locker"
|
2023-08-11 12:28:05 +00:00
|
|
|
"github.com/databricks/cli/libs/log"
|
2023-05-16 16:35:39 +00:00
|
|
|
"github.com/databricks/cli/libs/terraform"
|
2022-11-24 20:41:57 +00:00
|
|
|
"github.com/databricks/databricks-sdk-go"
|
2023-03-29 18:46:09 +00:00
|
|
|
sdkconfig "github.com/databricks/databricks-sdk-go/config"
|
2022-12-15 14:12:47 +00:00
|
|
|
"github.com/hashicorp/terraform-exec/tfexec"
|
2022-11-18 09:57:31 +00:00
|
|
|
)
|
|
|
|
|
2023-08-18 08:07:25 +00:00
|
|
|
const internalFolder = ".internal"
|
|
|
|
|
2022-11-18 09:57:31 +00:00
|
|
|
type Bundle struct {
|
|
|
|
Config config.Root
|
2022-11-23 14:20:03 +00:00
|
|
|
|
|
|
|
// Store a pointer to the workspace client.
|
|
|
|
// It can be initialized on demand after loading the configuration.
|
|
|
|
clientOnce sync.Once
|
2022-11-24 20:41:57 +00:00
|
|
|
client *databricks.WorkspaceClient
|
2022-12-15 14:12:47 +00:00
|
|
|
|
|
|
|
// Stores an initialized copy of this bundle's Terraform wrapper.
|
|
|
|
Terraform *tfexec.Terraform
|
2023-03-22 15:37:26 +00:00
|
|
|
|
|
|
|
// Stores the locker responsible for acquiring/releasing a deployment lock.
|
|
|
|
Locker *locker.Locker
|
2023-04-06 10:54:58 +00:00
|
|
|
|
|
|
|
Plan *terraform.Plan
|
|
|
|
|
|
|
|
// if true, we skip approval checks for deploy, destroy resources and delete
|
|
|
|
// files
|
|
|
|
AutoApprove bool
|
2022-11-18 09:57:31 +00:00
|
|
|
}
|
|
|
|
|
2023-08-02 17:22:47 +00:00
|
|
|
const ExtraIncludePathsKey string = "DATABRICKS_BUNDLE_INCLUDES"
|
|
|
|
|
2023-08-11 12:28:05 +00:00
|
|
|
func Load(ctx context.Context, path string) (*Bundle, error) {
|
2023-04-12 14:17:13 +00:00
|
|
|
bundle := &Bundle{}
|
2023-08-02 17:22:47 +00:00
|
|
|
stat, err := os.Stat(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-07-18 10:16:34 +00:00
|
|
|
configFile, err := config.FileNames.FindInPath(path)
|
|
|
|
if err != nil {
|
2023-08-02 17:22:47 +00:00
|
|
|
_, hasIncludePathEnv := os.LookupEnv(ExtraIncludePathsKey)
|
|
|
|
_, hasBundleRootEnv := os.LookupEnv(envBundleRoot)
|
|
|
|
if hasIncludePathEnv && hasBundleRootEnv && stat.IsDir() {
|
2023-08-11 12:28:05 +00:00
|
|
|
log.Debugf(ctx, "No bundle configuration; using bundle root: %s", path)
|
2023-08-02 17:22:47 +00:00
|
|
|
bundle.Config = config.Root{
|
|
|
|
Path: path,
|
|
|
|
Bundle: config.Bundle{
|
|
|
|
Name: filepath.Base(path),
|
|
|
|
},
|
|
|
|
}
|
|
|
|
return bundle, nil
|
|
|
|
}
|
2023-07-18 10:16:34 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2023-08-11 12:28:05 +00:00
|
|
|
log.Debugf(ctx, "Loading bundle configuration from: %s", configFile)
|
2023-07-18 10:16:34 +00:00
|
|
|
err = bundle.Config.Load(configFile)
|
2022-11-18 09:57:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return bundle, nil
|
|
|
|
}
|
2022-11-21 14:39:53 +00:00
|
|
|
|
2023-01-27 15:57:39 +00:00
|
|
|
// MustLoad returns a bundle configuration.
|
|
|
|
// It returns an error if a bundle was not found or could not be loaded.
|
2023-08-11 12:28:05 +00:00
|
|
|
func MustLoad(ctx context.Context) (*Bundle, error) {
|
2023-01-27 15:57:39 +00:00
|
|
|
root, err := mustGetRoot()
|
2022-11-21 14:39:53 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-01-27 15:57:39 +00:00
|
|
|
|
2023-08-11 12:28:05 +00:00
|
|
|
return Load(ctx, root)
|
2023-01-27 15:57:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TryLoad returns a bundle configuration if there is one, but doesn't fail if there isn't one.
|
|
|
|
// It returns an error if a bundle was found but could not be loaded.
|
|
|
|
// It returns a `nil` bundle if a bundle was not found.
|
2023-08-11 12:28:05 +00:00
|
|
|
func TryLoad(ctx context.Context) (*Bundle, error) {
|
2023-01-27 15:57:39 +00:00
|
|
|
root, err := tryGetRoot()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// No root is fine in this function.
|
|
|
|
if root == "" {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2022-11-21 14:39:53 +00:00
|
|
|
|
2023-08-11 12:28:05 +00:00
|
|
|
return Load(ctx, root)
|
2022-11-21 14:39:53 +00:00
|
|
|
}
|
|
|
|
|
2022-11-24 20:41:57 +00:00
|
|
|
func (b *Bundle) WorkspaceClient() *databricks.WorkspaceClient {
|
2022-11-23 14:20:03 +00:00
|
|
|
b.clientOnce.Do(func() {
|
2022-11-24 20:41:57 +00:00
|
|
|
var err error
|
|
|
|
b.client, err = b.Config.Workspace.Client()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2022-11-23 14:20:03 +00:00
|
|
|
})
|
|
|
|
return b.client
|
|
|
|
}
|
2022-11-30 13:40:41 +00:00
|
|
|
|
|
|
|
// CacheDir returns directory to use for temporary files for this bundle.
|
2023-08-17 15:22:32 +00:00
|
|
|
// Scoped to the bundle's target.
|
2022-12-15 16:30:33 +00:00
|
|
|
func (b *Bundle) CacheDir(paths ...string) (string, error) {
|
2023-08-17 15:22:32 +00:00
|
|
|
if b.Config.Bundle.Target == "" {
|
|
|
|
panic("target not set")
|
2022-11-30 13:40:41 +00:00
|
|
|
}
|
|
|
|
|
2023-06-21 07:53:54 +00:00
|
|
|
cacheDirName, exists := os.LookupEnv("DATABRICKS_BUNDLE_TMP")
|
|
|
|
|
|
|
|
if !exists || cacheDirName == "" {
|
|
|
|
cacheDirName = filepath.Join(
|
|
|
|
// Anchor at bundle root directory.
|
|
|
|
b.Config.Path,
|
|
|
|
// Static cache directory.
|
|
|
|
".databricks",
|
|
|
|
"bundle",
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-12-15 16:30:33 +00:00
|
|
|
// Fixed components of the result path.
|
|
|
|
parts := []string{
|
|
|
|
cacheDirName,
|
2023-08-17 15:22:32 +00:00
|
|
|
// Scope with target name.
|
|
|
|
b.Config.Bundle.Target,
|
2022-12-15 16:30:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Append dynamic components of the result path.
|
|
|
|
parts = append(parts, paths...)
|
|
|
|
|
2022-11-30 13:40:41 +00:00
|
|
|
// Make directory if it doesn't exist yet.
|
2022-12-15 16:30:33 +00:00
|
|
|
dir := filepath.Join(parts...)
|
2022-11-30 13:40:41 +00:00
|
|
|
err := os.MkdirAll(dir, 0700)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
return dir, nil
|
|
|
|
}
|
2023-03-29 14:36:35 +00:00
|
|
|
|
2023-08-18 08:07:25 +00:00
|
|
|
// 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() (string, error) {
|
|
|
|
cacheDir, err := b.CacheDir()
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
dir := filepath.Join(cacheDir, internalFolder)
|
|
|
|
err = os.MkdirAll(dir, 0700)
|
|
|
|
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() ([]string, error) {
|
|
|
|
internalDir, err := b.InternalDir()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
internalDirRel, err := filepath.Rel(b.Config.Path, internalDir)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return append(b.Config.Sync.Include, filepath.ToSlash(filepath.Join(internalDirRel, "*.*"))), nil
|
|
|
|
}
|
|
|
|
|
2023-03-29 14:36:35 +00:00
|
|
|
func (b *Bundle) GitRepository() (*git.Repository, error) {
|
|
|
|
rootPath, err := folders.FindDirWithLeaf(b.Config.Path, ".git")
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to locate repository root: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return git.NewRepository(rootPath)
|
|
|
|
}
|
2023-03-29 18:46:09 +00:00
|
|
|
|
|
|
|
// 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, fmt.Errorf("workspace client not initialized yet")
|
|
|
|
}
|
|
|
|
|
|
|
|
cfg := b.client.Config
|
|
|
|
out := make(map[string]string)
|
|
|
|
for _, attr := range sdkconfig.ConfigAttributes {
|
|
|
|
// Ignore profile so that downstream tools don't try and reload
|
|
|
|
// the profile even though we know the current configuration is valid.
|
|
|
|
if attr.Name == "profile" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if len(attr.EnvVars) == 0 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if attr.IsZero(cfg) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
out[attr.EnvVars[0]] = attr.GetString(cfg)
|
|
|
|
}
|
|
|
|
|
|
|
|
return out, nil
|
|
|
|
}
|