2022-11-18 09:57:31 +00:00
|
|
|
package config
|
|
|
|
|
|
|
|
import (
|
2023-05-15 09:34:05 +00:00
|
|
|
"fmt"
|
2022-11-18 09:57:31 +00:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2023-05-15 09:34:05 +00:00
|
|
|
"strings"
|
2022-11-18 09:57:31 +00:00
|
|
|
|
2023-05-16 16:35:39 +00:00
|
|
|
"github.com/databricks/cli/bundle/config/variable"
|
2023-08-23 16:47:07 +00:00
|
|
|
"github.com/databricks/databricks-sdk-go/service/jobs"
|
2022-11-18 09:57:31 +00:00
|
|
|
"github.com/ghodss/yaml"
|
|
|
|
"github.com/imdario/mergo"
|
|
|
|
)
|
|
|
|
|
2023-07-18 10:16:34 +00:00
|
|
|
type ConfigFileNames []string
|
|
|
|
|
|
|
|
// FileNames contains allowed names of bundle configuration files.
|
|
|
|
var FileNames = ConfigFileNames{"databricks.yml", "databricks.yaml", "bundle.yml", "bundle.yaml"}
|
|
|
|
|
|
|
|
func (c ConfigFileNames) FindInPath(path string) (string, error) {
|
|
|
|
result := ""
|
|
|
|
var firstErr error
|
|
|
|
|
|
|
|
for _, file := range c {
|
|
|
|
filePath := filepath.Join(path, file)
|
|
|
|
_, err := os.Stat(filePath)
|
|
|
|
if err == nil {
|
|
|
|
if result != "" {
|
|
|
|
return "", fmt.Errorf("multiple bundle root configuration files found in %s", path)
|
|
|
|
}
|
|
|
|
result = filePath
|
|
|
|
} else {
|
|
|
|
if firstErr == nil {
|
|
|
|
firstErr = err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if result == "" {
|
|
|
|
return "", firstErr
|
|
|
|
}
|
|
|
|
return result, nil
|
|
|
|
}
|
2022-11-18 09:57:31 +00:00
|
|
|
|
|
|
|
type Root struct {
|
|
|
|
// Path contains the directory path to the root of the bundle.
|
2023-07-18 10:16:34 +00:00
|
|
|
// It is set when loading `databricks.yml`.
|
2023-04-12 14:17:13 +00:00
|
|
|
Path string `json:"-" bundle:"readonly"`
|
2022-11-18 09:57:31 +00:00
|
|
|
|
2023-05-15 09:34:05 +00:00
|
|
|
// Contains user defined variables
|
|
|
|
Variables map[string]*variable.Variable `json:"variables,omitempty"`
|
|
|
|
|
2022-11-18 09:57:31 +00:00
|
|
|
// Bundle contains details about this bundle, such as its name,
|
|
|
|
// version of the spec (TODO), default cluster, default warehouse, etc.
|
|
|
|
Bundle Bundle `json:"bundle"`
|
|
|
|
|
|
|
|
// Include specifies a list of patterns of file names to load and
|
2023-07-25 08:00:46 +00:00
|
|
|
// merge into the this configuration. Only includes defined in the root
|
|
|
|
// `databricks.yml` are processed. Defaults to an empty list.
|
2022-11-18 09:57:31 +00:00
|
|
|
Include []string `json:"include,omitempty"`
|
|
|
|
|
|
|
|
// Workspace contains details about the workspace to connect to
|
|
|
|
// and paths in the workspace tree to use for this bundle.
|
2023-07-07 13:10:25 +00:00
|
|
|
Workspace Workspace `json:"workspace,omitempty"`
|
2022-11-18 09:57:31 +00:00
|
|
|
|
2022-11-30 13:15:22 +00:00
|
|
|
// Artifacts contains a description of all code artifacts in this bundle.
|
2023-09-04 09:55:01 +00:00
|
|
|
Artifacts Artifacts `json:"artifacts,omitempty"`
|
2022-11-30 13:15:22 +00:00
|
|
|
|
2022-11-18 09:57:31 +00:00
|
|
|
// Resources contains a description of all Databricks resources
|
2022-12-01 21:39:15 +00:00
|
|
|
// to deploy in this bundle (e.g. jobs, pipelines, etc.).
|
2023-01-20 15:55:44 +00:00
|
|
|
Resources Resources `json:"resources,omitempty"`
|
2022-11-18 09:57:31 +00:00
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
// Targets can be used to differentiate settings and resources between
|
|
|
|
// bundle deployment targets (e.g. development, staging, production).
|
2022-11-18 09:57:31 +00:00
|
|
|
// If not specified, the code below initializes this field with a
|
2023-08-17 15:22:32 +00:00
|
|
|
// single default-initialized target called "default".
|
|
|
|
Targets map[string]*Target `json:"targets,omitempty"`
|
|
|
|
|
|
|
|
// DEPRECATED. Left for backward compatibility with Targets
|
|
|
|
Environments map[string]*Target `json:"environments,omitempty"`
|
2023-08-18 08:07:25 +00:00
|
|
|
|
|
|
|
// Sync section specifies options for files synchronization
|
|
|
|
Sync Sync `json:"sync"`
|
2023-08-23 16:47:07 +00:00
|
|
|
|
|
|
|
// RunAs section allows to define an execution identity for jobs and pipelines runs
|
|
|
|
RunAs *jobs.JobRunAs `json:"run_as,omitempty"`
|
2022-11-18 09:57:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func Load(path string) (*Root, error) {
|
|
|
|
var r Root
|
|
|
|
|
|
|
|
stat, err := os.Stat(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// If we were given a directory, assume this is the bundle root.
|
|
|
|
if stat.IsDir() {
|
2023-07-18 10:16:34 +00:00
|
|
|
path, err = FileNames.FindInPath(path)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-11-18 09:57:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := r.Load(path); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &r, nil
|
|
|
|
}
|
|
|
|
|
2023-04-12 14:17:13 +00:00
|
|
|
// SetConfigFilePath configures the path that its configuration
|
|
|
|
// was loaded from in configuration leafs that require it.
|
|
|
|
func (r *Root) SetConfigFilePath(path string) {
|
|
|
|
r.Resources.SetConfigFilePath(path)
|
2023-09-04 09:55:01 +00:00
|
|
|
if r.Artifacts != nil {
|
|
|
|
r.Artifacts.SetConfigFilePath(path)
|
|
|
|
}
|
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
if r.Targets != nil {
|
|
|
|
for _, env := range r.Targets {
|
2023-06-08 20:55:49 +00:00
|
|
|
if env == nil {
|
|
|
|
continue
|
|
|
|
}
|
2023-04-12 14:17:13 +00:00
|
|
|
if env.Resources != nil {
|
|
|
|
env.Resources.SetConfigFilePath(path)
|
|
|
|
}
|
2023-09-04 09:55:01 +00:00
|
|
|
if env.Artifacts != nil {
|
|
|
|
env.Artifacts.SetConfigFilePath(path)
|
|
|
|
}
|
2023-04-12 14:17:13 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-05-15 09:34:05 +00:00
|
|
|
// Initializes variables using values passed from the command line flag
|
|
|
|
// Input has to be a string of the form `foo=bar`. In this case the variable with
|
|
|
|
// name `foo` is assigned the value `bar`
|
|
|
|
func (r *Root) InitializeVariables(vars []string) error {
|
|
|
|
for _, variable := range vars {
|
|
|
|
parsedVariable := strings.SplitN(variable, "=", 2)
|
|
|
|
if len(parsedVariable) != 2 {
|
|
|
|
return fmt.Errorf("unexpected flag value for variable assignment: %s", variable)
|
|
|
|
}
|
|
|
|
name := parsedVariable[0]
|
|
|
|
val := parsedVariable[1]
|
|
|
|
|
|
|
|
if _, ok := r.Variables[name]; !ok {
|
|
|
|
return fmt.Errorf("variable %s has not been defined", name)
|
|
|
|
}
|
|
|
|
err := r.Variables[name].Set(val)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to assign %s to %s: %s", val, name, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-04-12 14:17:13 +00:00
|
|
|
func (r *Root) Load(path string) error {
|
|
|
|
raw, err := os.ReadFile(path)
|
2022-11-18 09:57:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
err = yaml.Unmarshal(raw, r)
|
|
|
|
if err != nil {
|
2023-07-07 10:22:58 +00:00
|
|
|
return fmt.Errorf("failed to load %s: %w", path, err)
|
2022-11-18 09:57:31 +00:00
|
|
|
}
|
2023-04-17 10:21:21 +00:00
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
if r.Environments != nil && r.Targets != nil {
|
|
|
|
return fmt.Errorf("both 'environments' and 'targets' are specified, only 'targets' should be used: %s", path)
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Environments != nil {
|
|
|
|
//TODO: add a command line notice that this is a deprecated option.
|
|
|
|
r.Targets = r.Environments
|
|
|
|
}
|
|
|
|
|
2023-04-12 14:17:13 +00:00
|
|
|
r.Path = filepath.Dir(path)
|
|
|
|
r.SetConfigFilePath(path)
|
2023-04-17 10:21:21 +00:00
|
|
|
|
|
|
|
_, err = r.Resources.VerifyUniqueResourceIdentifiers()
|
|
|
|
return err
|
2022-11-18 09:57:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Root) Merge(other *Root) error {
|
2023-09-04 09:55:01 +00:00
|
|
|
err := r.Sync.Merge(r, other)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
other.Sync = Sync{}
|
|
|
|
|
2023-04-12 14:17:13 +00:00
|
|
|
// TODO: when hooking into merge semantics, disallow setting path on the target instance.
|
|
|
|
other.Path = ""
|
|
|
|
|
2023-04-17 10:21:21 +00:00
|
|
|
// Check for safe merge, protecting against duplicate resource identifiers
|
2023-09-04 09:55:01 +00:00
|
|
|
err = r.Resources.VerifySafeMerge(&other.Resources)
|
2023-04-17 10:21:21 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-11-18 09:57:31 +00:00
|
|
|
// TODO: define and test semantics for merging.
|
2023-08-15 09:58:54 +00:00
|
|
|
return mergo.Merge(r, other, mergo.WithOverride)
|
2022-11-18 09:57:31 +00:00
|
|
|
}
|
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
func (r *Root) MergeTargetOverrides(target *Target) error {
|
2022-11-18 09:57:31 +00:00
|
|
|
var err error
|
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
// Target may be nil if it's empty.
|
|
|
|
if target == nil {
|
2022-12-22 14:31:32 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
if target.Bundle != nil {
|
|
|
|
err = mergo.Merge(&r.Bundle, target.Bundle, mergo.WithOverride)
|
2022-11-18 09:57:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
if target.Workspace != nil {
|
|
|
|
err = mergo.Merge(&r.Workspace, target.Workspace, mergo.WithOverride)
|
2022-11-18 09:57:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
if target.Artifacts != nil {
|
|
|
|
err = mergo.Merge(&r.Artifacts, target.Artifacts, mergo.WithOverride, mergo.WithAppendSlice)
|
2022-11-30 13:15:22 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
if target.Resources != nil {
|
|
|
|
err = mergo.Merge(&r.Resources, target.Resources, mergo.WithOverride, mergo.WithAppendSlice)
|
2022-11-18 09:57:31 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2023-08-14 06:43:45 +00:00
|
|
|
|
|
|
|
err = r.Resources.MergeJobClusters()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2022-11-18 09:57:31 +00:00
|
|
|
}
|
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
if target.Variables != nil {
|
|
|
|
for k, v := range target.Variables {
|
2023-05-15 12:07:18 +00:00
|
|
|
variable, ok := r.Variables[k]
|
|
|
|
if !ok {
|
|
|
|
return fmt.Errorf("variable %s is not defined but is assigned a value", k)
|
|
|
|
}
|
|
|
|
// we only allow overrides of the default value for a variable
|
|
|
|
defaultVal := v
|
|
|
|
variable.Default = &defaultVal
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-23 16:47:07 +00:00
|
|
|
if target.RunAs != nil {
|
|
|
|
r.RunAs = target.RunAs
|
|
|
|
}
|
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
if target.Mode != "" {
|
|
|
|
r.Bundle.Mode = target.Mode
|
2023-07-12 06:51:54 +00:00
|
|
|
}
|
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
if target.ComputeID != "" {
|
|
|
|
r.Bundle.ComputeID = target.ComputeID
|
2023-07-12 06:51:54 +00:00
|
|
|
}
|
|
|
|
|
2023-07-30 12:44:33 +00:00
|
|
|
git := &r.Bundle.Git
|
2023-08-17 15:22:32 +00:00
|
|
|
if target.Git.Branch != "" {
|
|
|
|
git.Branch = target.Git.Branch
|
2023-07-30 12:44:33 +00:00
|
|
|
git.Inferred = false
|
|
|
|
}
|
2023-08-17 15:22:32 +00:00
|
|
|
if target.Git.Commit != "" {
|
|
|
|
git.Commit = target.Git.Commit
|
2023-07-30 12:44:33 +00:00
|
|
|
}
|
2023-08-17 15:22:32 +00:00
|
|
|
if target.Git.OriginURL != "" {
|
|
|
|
git.OriginURL = target.Git.OriginURL
|
2023-07-30 12:44:33 +00:00
|
|
|
}
|
|
|
|
|
2022-11-18 09:57:31 +00:00
|
|
|
return nil
|
|
|
|
}
|