mirror of https://github.com/databricks/cli.git
Add configurable presets for name prefixes, tags, etc. (#1490)
## Changes This adds configurable transformations based on the transformations currently seen in `mode: development`. Example databricks.yml showcasing how some transformations: ``` bundle: name: my_bundle targets: dev: presets: prefix: "myprefix_" # prefix all resource names with myprefix_ pipelines_development: true # set development to true by default for pipelines trigger_pause_status: PAUSED # set pause_status to PAUSED by default for all triggers and schedules jobs_max_concurrent_runs: 10 # set max_concurrent runs to 10 by default for all jobs tags: dev: true ``` ## Tests * Existing process_target_mode tests that were adapted to use this new code * Unit tests specific for the new mutator * Unit tests for config loading and merging * Manual e2e testing
This commit is contained in:
parent
07627023f5
commit
78d0ac5c6a
|
@ -0,0 +1,209 @@
|
||||||
|
package mutator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"path"
|
||||||
|
"slices"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/bundle"
|
||||||
|
"github.com/databricks/cli/bundle/config"
|
||||||
|
"github.com/databricks/cli/libs/diag"
|
||||||
|
"github.com/databricks/cli/libs/dyn"
|
||||||
|
"github.com/databricks/cli/libs/textutil"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/catalog"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/jobs"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/ml"
|
||||||
|
)
|
||||||
|
|
||||||
|
type applyPresets struct{}
|
||||||
|
|
||||||
|
// Apply all presets, e.g. the prefix presets that
|
||||||
|
// adds a prefix to all names of all resources.
|
||||||
|
func ApplyPresets() *applyPresets {
|
||||||
|
return &applyPresets{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tag struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *applyPresets) Name() string {
|
||||||
|
return "ApplyPresets"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *applyPresets) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||||
|
if d := validatePauseStatus(b); d != nil {
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
r := b.Config.Resources
|
||||||
|
t := b.Config.Presets
|
||||||
|
prefix := t.NamePrefix
|
||||||
|
tags := toTagArray(t.Tags)
|
||||||
|
|
||||||
|
// Jobs presets: Prefix, Tags, JobsMaxConcurrentRuns, TriggerPauseStatus
|
||||||
|
for _, j := range r.Jobs {
|
||||||
|
j.Name = prefix + j.Name
|
||||||
|
if j.Tags == nil {
|
||||||
|
j.Tags = make(map[string]string)
|
||||||
|
}
|
||||||
|
for _, tag := range tags {
|
||||||
|
if j.Tags[tag.Key] == "" {
|
||||||
|
j.Tags[tag.Key] = tag.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if j.MaxConcurrentRuns == 0 {
|
||||||
|
j.MaxConcurrentRuns = t.JobsMaxConcurrentRuns
|
||||||
|
}
|
||||||
|
if t.TriggerPauseStatus != "" {
|
||||||
|
paused := jobs.PauseStatusPaused
|
||||||
|
if t.TriggerPauseStatus == config.Unpaused {
|
||||||
|
paused = jobs.PauseStatusUnpaused
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.Schedule != nil && j.Schedule.PauseStatus == "" {
|
||||||
|
j.Schedule.PauseStatus = paused
|
||||||
|
}
|
||||||
|
if j.Continuous != nil && j.Continuous.PauseStatus == "" {
|
||||||
|
j.Continuous.PauseStatus = paused
|
||||||
|
}
|
||||||
|
if j.Trigger != nil && j.Trigger.PauseStatus == "" {
|
||||||
|
j.Trigger.PauseStatus = paused
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pipelines presets: Prefix, PipelinesDevelopment
|
||||||
|
for i := range r.Pipelines {
|
||||||
|
r.Pipelines[i].Name = prefix + r.Pipelines[i].Name
|
||||||
|
if config.IsExplicitlyEnabled(t.PipelinesDevelopment) {
|
||||||
|
r.Pipelines[i].Development = true
|
||||||
|
}
|
||||||
|
if t.TriggerPauseStatus == config.Paused {
|
||||||
|
r.Pipelines[i].Continuous = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// As of 2024-06, pipelines don't yet support tags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Models presets: Prefix, Tags
|
||||||
|
for _, m := range r.Models {
|
||||||
|
m.Name = prefix + m.Name
|
||||||
|
for _, t := range tags {
|
||||||
|
exists := slices.ContainsFunc(m.Tags, func(modelTag ml.ModelTag) bool {
|
||||||
|
return modelTag.Key == t.Key
|
||||||
|
})
|
||||||
|
if !exists {
|
||||||
|
// Only add this tag if the resource didn't include any tag that overrides its value.
|
||||||
|
m.Tags = append(m.Tags, ml.ModelTag{Key: t.Key, Value: t.Value})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Experiments presets: Prefix, Tags
|
||||||
|
for _, e := range r.Experiments {
|
||||||
|
filepath := e.Name
|
||||||
|
dir := path.Dir(filepath)
|
||||||
|
base := path.Base(filepath)
|
||||||
|
if dir == "." {
|
||||||
|
e.Name = prefix + base
|
||||||
|
} else {
|
||||||
|
e.Name = dir + "/" + prefix + base
|
||||||
|
}
|
||||||
|
for _, t := range tags {
|
||||||
|
exists := false
|
||||||
|
for _, experimentTag := range e.Tags {
|
||||||
|
if experimentTag.Key == t.Key {
|
||||||
|
exists = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
e.Tags = append(e.Tags, ml.ExperimentTag{Key: t.Key, Value: t.Value})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model serving endpoint presets: Prefix
|
||||||
|
for i := range r.ModelServingEndpoints {
|
||||||
|
r.ModelServingEndpoints[i].Name = normalizePrefix(prefix) + r.ModelServingEndpoints[i].Name
|
||||||
|
|
||||||
|
// As of 2024-06, model serving endpoints don't yet support tags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registered models presets: Prefix
|
||||||
|
for i := range r.RegisteredModels {
|
||||||
|
r.RegisteredModels[i].Name = normalizePrefix(prefix) + r.RegisteredModels[i].Name
|
||||||
|
|
||||||
|
// As of 2024-06, registered models don't yet support tags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quality monitors presets: Prefix
|
||||||
|
if t.TriggerPauseStatus == config.Paused {
|
||||||
|
for i := range r.QualityMonitors {
|
||||||
|
// Remove all schedules from monitors, since they don't support pausing/unpausing.
|
||||||
|
// Quality monitors might support the "pause" property in the future, so at the
|
||||||
|
// CLI level we do respect that property if it is set to "unpaused."
|
||||||
|
if r.QualityMonitors[i].Schedule != nil && r.QualityMonitors[i].Schedule.PauseStatus != catalog.MonitorCronSchedulePauseStatusUnpaused {
|
||||||
|
r.QualityMonitors[i].Schedule = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schemas: Prefix
|
||||||
|
for i := range r.Schemas {
|
||||||
|
prefix = "dev_" + b.Config.Workspace.CurrentUser.ShortName + "_"
|
||||||
|
r.Schemas[i].Name = prefix + r.Schemas[i].Name
|
||||||
|
// HTTP API for schemas doesn't yet support tags. It's only supported in
|
||||||
|
// the Databricks UI and via the SQL API.
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePauseStatus(b *bundle.Bundle) diag.Diagnostics {
|
||||||
|
p := b.Config.Presets.TriggerPauseStatus
|
||||||
|
if p == "" || p == config.Paused || p == config.Unpaused {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return diag.Diagnostics{{
|
||||||
|
Summary: "Invalid value for trigger_pause_status, should be PAUSED or UNPAUSED",
|
||||||
|
Severity: diag.Error,
|
||||||
|
Locations: []dyn.Location{b.Config.GetLocation("presets.trigger_pause_status")},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toTagArray converts a map of tags to an array of tags.
|
||||||
|
// We sort tags so ensure stable ordering.
|
||||||
|
func toTagArray(tags map[string]string) []Tag {
|
||||||
|
var tagArray []Tag
|
||||||
|
if tags == nil {
|
||||||
|
return tagArray
|
||||||
|
}
|
||||||
|
for key, value := range tags {
|
||||||
|
tagArray = append(tagArray, Tag{Key: key, Value: value})
|
||||||
|
}
|
||||||
|
sort.Slice(tagArray, func(i, j int) bool {
|
||||||
|
return tagArray[i].Key < tagArray[j].Key
|
||||||
|
})
|
||||||
|
return tagArray
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizePrefix prefixes strings like '[dev lennart] ' to 'dev_lennart_'.
|
||||||
|
// We leave unicode letters and numbers but remove all "special characters."
|
||||||
|
func normalizePrefix(prefix string) string {
|
||||||
|
prefix = strings.ReplaceAll(prefix, "[", "")
|
||||||
|
prefix = strings.Trim(prefix, " ")
|
||||||
|
|
||||||
|
// If the prefix ends with a ']', we add an underscore to the end.
|
||||||
|
// This makes sure that we get names like "dev_user_endpoint" instead of "dev_userendpoint"
|
||||||
|
suffix := ""
|
||||||
|
if strings.HasSuffix(prefix, "]") {
|
||||||
|
suffix = "_"
|
||||||
|
}
|
||||||
|
|
||||||
|
return textutil.NormalizeString(prefix) + suffix
|
||||||
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
package mutator_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/bundle"
|
||||||
|
"github.com/databricks/cli/bundle/config"
|
||||||
|
"github.com/databricks/cli/bundle/config/mutator"
|
||||||
|
"github.com/databricks/cli/bundle/config/resources"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/jobs"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestApplyPresetsPrefix(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
prefix string
|
||||||
|
job *resources.Job
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "add prefix to job",
|
||||||
|
prefix: "prefix-",
|
||||||
|
job: &resources.Job{
|
||||||
|
JobSettings: &jobs.JobSettings{
|
||||||
|
Name: "job1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: "prefix-job1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "add empty prefix to job",
|
||||||
|
prefix: "",
|
||||||
|
job: &resources.Job{
|
||||||
|
JobSettings: &jobs.JobSettings{
|
||||||
|
Name: "job1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: "job1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
b := &bundle.Bundle{
|
||||||
|
Config: config.Root{
|
||||||
|
Resources: config.Resources{
|
||||||
|
Jobs: map[string]*resources.Job{
|
||||||
|
"job1": tt.job,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Presets: config.Presets{
|
||||||
|
NamePrefix: tt.prefix,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
diag := bundle.Apply(ctx, b, mutator.ApplyPresets())
|
||||||
|
|
||||||
|
if diag.HasError() {
|
||||||
|
t.Fatalf("unexpected error: %v", diag)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, tt.want, b.Config.Resources.Jobs["job1"].Name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyPresetsTags(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
tags map[string]string
|
||||||
|
job *resources.Job
|
||||||
|
want map[string]string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "add tags to job",
|
||||||
|
tags: map[string]string{"env": "dev"},
|
||||||
|
job: &resources.Job{
|
||||||
|
JobSettings: &jobs.JobSettings{
|
||||||
|
Name: "job1",
|
||||||
|
Tags: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: map[string]string{"env": "dev"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "merge tags with existing job tags",
|
||||||
|
tags: map[string]string{"env": "dev"},
|
||||||
|
job: &resources.Job{
|
||||||
|
JobSettings: &jobs.JobSettings{
|
||||||
|
Name: "job1",
|
||||||
|
Tags: map[string]string{"team": "data"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: map[string]string{"env": "dev", "team": "data"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "don't override existing job tags",
|
||||||
|
tags: map[string]string{"env": "dev"},
|
||||||
|
job: &resources.Job{
|
||||||
|
JobSettings: &jobs.JobSettings{
|
||||||
|
Name: "job1",
|
||||||
|
Tags: map[string]string{"env": "prod"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: map[string]string{"env": "prod"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
b := &bundle.Bundle{
|
||||||
|
Config: config.Root{
|
||||||
|
Resources: config.Resources{
|
||||||
|
Jobs: map[string]*resources.Job{
|
||||||
|
"job1": tt.job,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Presets: config.Presets{
|
||||||
|
Tags: tt.tags,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
diag := bundle.Apply(ctx, b, mutator.ApplyPresets())
|
||||||
|
|
||||||
|
if diag.HasError() {
|
||||||
|
t.Fatalf("unexpected error: %v", diag)
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := b.Config.Resources.Jobs["job1"].Tags
|
||||||
|
require.Equal(t, tt.want, tags)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyPresetsJobsMaxConcurrentRuns(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
job *resources.Job
|
||||||
|
setting int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "set max concurrent runs",
|
||||||
|
job: &resources.Job{
|
||||||
|
JobSettings: &jobs.JobSettings{
|
||||||
|
Name: "job1",
|
||||||
|
MaxConcurrentRuns: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setting: 5,
|
||||||
|
want: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "do not override existing max concurrent runs",
|
||||||
|
job: &resources.Job{
|
||||||
|
JobSettings: &jobs.JobSettings{
|
||||||
|
Name: "job1",
|
||||||
|
MaxConcurrentRuns: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setting: 5,
|
||||||
|
want: 3,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
b := &bundle.Bundle{
|
||||||
|
Config: config.Root{
|
||||||
|
Resources: config.Resources{
|
||||||
|
Jobs: map[string]*resources.Job{
|
||||||
|
"job1": tt.job,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Presets: config.Presets{
|
||||||
|
JobsMaxConcurrentRuns: tt.setting,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctx := context.Background()
|
||||||
|
diag := bundle.Apply(ctx, b, mutator.ApplyPresets())
|
||||||
|
|
||||||
|
if diag.HasError() {
|
||||||
|
t.Fatalf("unexpected error: %v", diag)
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, tt.want, b.Config.Resources.Jobs["job1"].MaxConcurrentRuns)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,17 +2,14 @@ package mutator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle"
|
"github.com/databricks/cli/bundle"
|
||||||
"github.com/databricks/cli/bundle/config"
|
"github.com/databricks/cli/bundle/config"
|
||||||
"github.com/databricks/cli/libs/auth"
|
"github.com/databricks/cli/libs/auth"
|
||||||
"github.com/databricks/cli/libs/diag"
|
"github.com/databricks/cli/libs/diag"
|
||||||
|
"github.com/databricks/cli/libs/dyn"
|
||||||
"github.com/databricks/cli/libs/log"
|
"github.com/databricks/cli/libs/log"
|
||||||
"github.com/databricks/databricks-sdk-go/service/catalog"
|
|
||||||
"github.com/databricks/databricks-sdk-go/service/jobs"
|
|
||||||
"github.com/databricks/databricks-sdk-go/service/ml"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type processTargetMode struct{}
|
type processTargetMode struct{}
|
||||||
|
@ -30,103 +27,75 @@ func (m *processTargetMode) Name() string {
|
||||||
// Mark all resources as being for 'development' purposes, i.e.
|
// Mark all resources as being for 'development' purposes, i.e.
|
||||||
// changing their their name, adding tags, and (in the future)
|
// changing their their name, adding tags, and (in the future)
|
||||||
// marking them as 'hidden' in the UI.
|
// marking them as 'hidden' in the UI.
|
||||||
func transformDevelopmentMode(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
func transformDevelopmentMode(ctx context.Context, b *bundle.Bundle) {
|
||||||
if !b.Config.Bundle.Deployment.Lock.IsExplicitlyEnabled() {
|
if !b.Config.Bundle.Deployment.Lock.IsExplicitlyEnabled() {
|
||||||
log.Infof(ctx, "Development mode: disabling deployment lock since bundle.deployment.lock.enabled is not set to true")
|
log.Infof(ctx, "Development mode: disabling deployment lock since bundle.deployment.lock.enabled is not set to true")
|
||||||
disabled := false
|
disabled := false
|
||||||
b.Config.Bundle.Deployment.Lock.Enabled = &disabled
|
b.Config.Bundle.Deployment.Lock.Enabled = &disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
r := b.Config.Resources
|
t := &b.Config.Presets
|
||||||
shortName := b.Config.Workspace.CurrentUser.ShortName
|
shortName := b.Config.Workspace.CurrentUser.ShortName
|
||||||
prefix := "[dev " + shortName + "] "
|
|
||||||
|
|
||||||
// Generate a normalized version of the short name that can be used as a tag value.
|
if t.NamePrefix == "" {
|
||||||
tagValue := b.Tagging.NormalizeValue(shortName)
|
t.NamePrefix = "[dev " + shortName + "] "
|
||||||
|
|
||||||
for i := range r.Jobs {
|
|
||||||
r.Jobs[i].Name = prefix + r.Jobs[i].Name
|
|
||||||
if r.Jobs[i].Tags == nil {
|
|
||||||
r.Jobs[i].Tags = make(map[string]string)
|
|
||||||
}
|
|
||||||
r.Jobs[i].Tags["dev"] = tagValue
|
|
||||||
if r.Jobs[i].MaxConcurrentRuns == 0 {
|
|
||||||
r.Jobs[i].MaxConcurrentRuns = developmentConcurrentRuns
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pause each job. As an exception, we don't pause jobs that are explicitly
|
|
||||||
// marked as "unpaused". This allows users to override the default behavior
|
|
||||||
// of the development mode.
|
|
||||||
if r.Jobs[i].Schedule != nil && r.Jobs[i].Schedule.PauseStatus != jobs.PauseStatusUnpaused {
|
|
||||||
r.Jobs[i].Schedule.PauseStatus = jobs.PauseStatusPaused
|
|
||||||
}
|
|
||||||
if r.Jobs[i].Continuous != nil && r.Jobs[i].Continuous.PauseStatus != jobs.PauseStatusUnpaused {
|
|
||||||
r.Jobs[i].Continuous.PauseStatus = jobs.PauseStatusPaused
|
|
||||||
}
|
|
||||||
if r.Jobs[i].Trigger != nil && r.Jobs[i].Trigger.PauseStatus != jobs.PauseStatusUnpaused {
|
|
||||||
r.Jobs[i].Trigger.PauseStatus = jobs.PauseStatusPaused
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range r.Pipelines {
|
if t.Tags == nil {
|
||||||
r.Pipelines[i].Name = prefix + r.Pipelines[i].Name
|
t.Tags = map[string]string{}
|
||||||
r.Pipelines[i].Development = true
|
}
|
||||||
r.Pipelines[i].Continuous = false
|
_, exists := t.Tags["dev"]
|
||||||
// (pipelines don't yet support tags)
|
if !exists {
|
||||||
|
t.Tags["dev"] = b.Tagging.NormalizeValue(shortName)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range r.Models {
|
if t.JobsMaxConcurrentRuns == 0 {
|
||||||
r.Models[i].Name = prefix + r.Models[i].Name
|
t.JobsMaxConcurrentRuns = developmentConcurrentRuns
|
||||||
r.Models[i].Tags = append(r.Models[i].Tags, ml.ModelTag{Key: "dev", Value: tagValue})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range r.Experiments {
|
if t.TriggerPauseStatus == "" {
|
||||||
filepath := r.Experiments[i].Name
|
t.TriggerPauseStatus = config.Paused
|
||||||
dir := path.Dir(filepath)
|
|
||||||
base := path.Base(filepath)
|
|
||||||
if dir == "." {
|
|
||||||
r.Experiments[i].Name = prefix + base
|
|
||||||
} else {
|
|
||||||
r.Experiments[i].Name = dir + "/" + prefix + base
|
|
||||||
}
|
|
||||||
r.Experiments[i].Tags = append(r.Experiments[i].Tags, ml.ExperimentTag{Key: "dev", Value: tagValue})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range r.ModelServingEndpoints {
|
if !config.IsExplicitlyDisabled(t.PipelinesDevelopment) {
|
||||||
prefix = "dev_" + b.Config.Workspace.CurrentUser.ShortName + "_"
|
enabled := true
|
||||||
r.ModelServingEndpoints[i].Name = prefix + r.ModelServingEndpoints[i].Name
|
t.PipelinesDevelopment = &enabled
|
||||||
// (model serving doesn't yet support tags)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range r.RegisteredModels {
|
|
||||||
prefix = "dev_" + b.Config.Workspace.CurrentUser.ShortName + "_"
|
|
||||||
r.RegisteredModels[i].Name = prefix + r.RegisteredModels[i].Name
|
|
||||||
// (registered models in Unity Catalog don't yet support tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range r.QualityMonitors {
|
|
||||||
// Remove all schedules from monitors, since they don't support pausing/unpausing.
|
|
||||||
// Quality monitors might support the "pause" property in the future, so at the
|
|
||||||
// CLI level we do respect that property if it is set to "unpaused".
|
|
||||||
if r.QualityMonitors[i].Schedule != nil && r.QualityMonitors[i].Schedule.PauseStatus != catalog.MonitorCronSchedulePauseStatusUnpaused {
|
|
||||||
r.QualityMonitors[i].Schedule = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range r.Schemas {
|
|
||||||
prefix = "dev_" + b.Config.Workspace.CurrentUser.ShortName + "_"
|
|
||||||
r.Schemas[i].Name = prefix + r.Schemas[i].Name
|
|
||||||
// HTTP API for schemas doesn't yet support tags. It's only supported in
|
|
||||||
// the Databricks UI and via the SQL API.
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateDevelopmentMode(b *bundle.Bundle) diag.Diagnostics {
|
func validateDevelopmentMode(b *bundle.Bundle) diag.Diagnostics {
|
||||||
|
p := b.Config.Presets
|
||||||
|
u := b.Config.Workspace.CurrentUser
|
||||||
|
|
||||||
|
// Make sure presets don't set the trigger status to UNPAUSED;
|
||||||
|
// this could be surprising since most users (and tools) expect triggers
|
||||||
|
// to be paused in development.
|
||||||
|
// (Note that there still is an exceptional case where users set the trigger
|
||||||
|
// status to UNPAUSED at the level of an individual object, whic hwas
|
||||||
|
// historically allowed.)
|
||||||
|
if p.TriggerPauseStatus == config.Unpaused {
|
||||||
|
return diag.Diagnostics{{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: "target with 'mode: development' cannot set trigger pause status to UNPAUSED by default",
|
||||||
|
Locations: []dyn.Location{b.Config.GetLocation("presets.trigger_pause_status")},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure this development copy has unique names and paths to avoid conflicts
|
||||||
if path := findNonUserPath(b); path != "" {
|
if path := findNonUserPath(b); path != "" {
|
||||||
return diag.Errorf("%s must start with '~/' or contain the current username when using 'mode: development'", path)
|
return diag.Errorf("%s must start with '~/' or contain the current username when using 'mode: development'", path)
|
||||||
}
|
}
|
||||||
|
if p.NamePrefix != "" && !strings.Contains(p.NamePrefix, u.ShortName) && !strings.Contains(p.NamePrefix, u.UserName) {
|
||||||
|
// Resources such as pipelines require a unique name, e.g. '[dev steve] my_pipeline'.
|
||||||
|
// For this reason we require the name prefix to contain the current username;
|
||||||
|
// it's a pitfall for users if they don't include it and later find out that
|
||||||
|
// only a single user can do development deployments.
|
||||||
|
return diag.Diagnostics{{
|
||||||
|
Severity: diag.Error,
|
||||||
|
Summary: "prefix should contain the current username or ${workspace.current_user.short_name} to ensure uniqueness when using 'mode: development'",
|
||||||
|
Locations: []dyn.Location{b.Config.GetLocation("presets.name_prefix")},
|
||||||
|
}}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,10 +152,11 @@ func (m *processTargetMode) Apply(ctx context.Context, b *bundle.Bundle) diag.Di
|
||||||
switch b.Config.Bundle.Mode {
|
switch b.Config.Bundle.Mode {
|
||||||
case config.Development:
|
case config.Development:
|
||||||
diags := validateDevelopmentMode(b)
|
diags := validateDevelopmentMode(b)
|
||||||
if diags != nil {
|
if diags.HasError() {
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
return transformDevelopmentMode(ctx, b)
|
transformDevelopmentMode(ctx, b)
|
||||||
|
return diags
|
||||||
case config.Production:
|
case config.Production:
|
||||||
isPrincipal := auth.IsServicePrincipal(b.Config.Workspace.CurrentUser.UserName)
|
isPrincipal := auth.IsServicePrincipal(b.Config.Workspace.CurrentUser.UserName)
|
||||||
return validateProductionMode(ctx, b, isPrincipal)
|
return validateProductionMode(ctx, b, isPrincipal)
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/databricks/cli/bundle"
|
"github.com/databricks/cli/bundle"
|
||||||
"github.com/databricks/cli/bundle/config"
|
"github.com/databricks/cli/bundle/config"
|
||||||
"github.com/databricks/cli/bundle/config/resources"
|
"github.com/databricks/cli/bundle/config/resources"
|
||||||
|
"github.com/databricks/cli/libs/diag"
|
||||||
"github.com/databricks/cli/libs/tags"
|
"github.com/databricks/cli/libs/tags"
|
||||||
sdkconfig "github.com/databricks/databricks-sdk-go/config"
|
sdkconfig "github.com/databricks/databricks-sdk-go/config"
|
||||||
"github.com/databricks/databricks-sdk-go/service/catalog"
|
"github.com/databricks/databricks-sdk-go/service/catalog"
|
||||||
|
@ -51,6 +52,7 @@ func mockBundle(mode config.Mode) *bundle.Bundle {
|
||||||
Schedule: &jobs.CronSchedule{
|
Schedule: &jobs.CronSchedule{
|
||||||
QuartzCronExpression: "* * * * *",
|
QuartzCronExpression: "* * * * *",
|
||||||
},
|
},
|
||||||
|
Tags: map[string]string{"existing": "tag"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"job2": {
|
"job2": {
|
||||||
|
@ -129,12 +131,13 @@ func mockBundle(mode config.Mode) *bundle.Bundle {
|
||||||
func TestProcessTargetModeDevelopment(t *testing.T) {
|
func TestProcessTargetModeDevelopment(t *testing.T) {
|
||||||
b := mockBundle(config.Development)
|
b := mockBundle(config.Development)
|
||||||
|
|
||||||
m := ProcessTargetMode()
|
m := bundle.Seq(ProcessTargetMode(), ApplyPresets())
|
||||||
diags := bundle.Apply(context.Background(), b, m)
|
diags := bundle.Apply(context.Background(), b, m)
|
||||||
require.NoError(t, diags.Error())
|
require.NoError(t, diags.Error())
|
||||||
|
|
||||||
// Job 1
|
// Job 1
|
||||||
assert.Equal(t, "[dev lennart] job1", b.Config.Resources.Jobs["job1"].Name)
|
assert.Equal(t, "[dev lennart] job1", b.Config.Resources.Jobs["job1"].Name)
|
||||||
|
assert.Equal(t, b.Config.Resources.Jobs["job1"].Tags["existing"], "tag")
|
||||||
assert.Equal(t, b.Config.Resources.Jobs["job1"].Tags["dev"], "lennart")
|
assert.Equal(t, b.Config.Resources.Jobs["job1"].Tags["dev"], "lennart")
|
||||||
assert.Equal(t, b.Config.Resources.Jobs["job1"].Schedule.PauseStatus, jobs.PauseStatusPaused)
|
assert.Equal(t, b.Config.Resources.Jobs["job1"].Schedule.PauseStatus, jobs.PauseStatusPaused)
|
||||||
|
|
||||||
|
@ -183,7 +186,8 @@ func TestProcessTargetModeDevelopmentTagNormalizationForAws(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
b.Config.Workspace.CurrentUser.ShortName = "Héllö wörld?!"
|
b.Config.Workspace.CurrentUser.ShortName = "Héllö wörld?!"
|
||||||
diags := bundle.Apply(context.Background(), b, ProcessTargetMode())
|
m := bundle.Seq(ProcessTargetMode(), ApplyPresets())
|
||||||
|
diags := bundle.Apply(context.Background(), b, m)
|
||||||
require.NoError(t, diags.Error())
|
require.NoError(t, diags.Error())
|
||||||
|
|
||||||
// Assert that tag normalization took place.
|
// Assert that tag normalization took place.
|
||||||
|
@ -197,7 +201,8 @@ func TestProcessTargetModeDevelopmentTagNormalizationForAzure(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
b.Config.Workspace.CurrentUser.ShortName = "Héllö wörld?!"
|
b.Config.Workspace.CurrentUser.ShortName = "Héllö wörld?!"
|
||||||
diags := bundle.Apply(context.Background(), b, ProcessTargetMode())
|
m := bundle.Seq(ProcessTargetMode(), ApplyPresets())
|
||||||
|
diags := bundle.Apply(context.Background(), b, m)
|
||||||
require.NoError(t, diags.Error())
|
require.NoError(t, diags.Error())
|
||||||
|
|
||||||
// Assert that tag normalization took place (Azure allows more characters than AWS).
|
// Assert that tag normalization took place (Azure allows more characters than AWS).
|
||||||
|
@ -211,17 +216,53 @@ func TestProcessTargetModeDevelopmentTagNormalizationForGcp(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
b.Config.Workspace.CurrentUser.ShortName = "Héllö wörld?!"
|
b.Config.Workspace.CurrentUser.ShortName = "Héllö wörld?!"
|
||||||
diags := bundle.Apply(context.Background(), b, ProcessTargetMode())
|
m := bundle.Seq(ProcessTargetMode(), ApplyPresets())
|
||||||
|
diags := bundle.Apply(context.Background(), b, m)
|
||||||
require.NoError(t, diags.Error())
|
require.NoError(t, diags.Error())
|
||||||
|
|
||||||
// Assert that tag normalization took place.
|
// Assert that tag normalization took place.
|
||||||
assert.Equal(t, "Hello_world", b.Config.Resources.Jobs["job1"].Tags["dev"])
|
assert.Equal(t, "Hello_world", b.Config.Resources.Jobs["job1"].Tags["dev"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateDevelopmentMode(t *testing.T) {
|
||||||
|
// Test with a valid development mode bundle
|
||||||
|
b := mockBundle(config.Development)
|
||||||
|
diags := validateDevelopmentMode(b)
|
||||||
|
require.NoError(t, diags.Error())
|
||||||
|
|
||||||
|
// Test with a bundle that has a non-user path
|
||||||
|
b.Config.Workspace.RootPath = "/Shared/.bundle/x/y/state"
|
||||||
|
diags = validateDevelopmentMode(b)
|
||||||
|
require.ErrorContains(t, diags.Error(), "root_path")
|
||||||
|
|
||||||
|
// Test with a bundle that has an unpaused trigger pause status
|
||||||
|
b = mockBundle(config.Development)
|
||||||
|
b.Config.Presets.TriggerPauseStatus = config.Unpaused
|
||||||
|
diags = validateDevelopmentMode(b)
|
||||||
|
require.ErrorContains(t, diags.Error(), "UNPAUSED")
|
||||||
|
|
||||||
|
// Test with a bundle that has a prefix not containing the username or short name
|
||||||
|
b = mockBundle(config.Development)
|
||||||
|
b.Config.Presets.NamePrefix = "[prod]"
|
||||||
|
diags = validateDevelopmentMode(b)
|
||||||
|
require.Len(t, diags, 1)
|
||||||
|
assert.Equal(t, diag.Error, diags[0].Severity)
|
||||||
|
assert.Contains(t, diags[0].Summary, "")
|
||||||
|
|
||||||
|
// Test with a bundle that has valid user paths
|
||||||
|
b = mockBundle(config.Development)
|
||||||
|
b.Config.Workspace.RootPath = "/Users/lennart@company.com/.bundle/x/y/state"
|
||||||
|
b.Config.Workspace.StatePath = "/Users/lennart@company.com/.bundle/x/y/state"
|
||||||
|
b.Config.Workspace.FilePath = "/Users/lennart@company.com/.bundle/x/y/files"
|
||||||
|
b.Config.Workspace.ArtifactPath = "/Users/lennart@company.com/.bundle/x/y/artifacts"
|
||||||
|
diags = validateDevelopmentMode(b)
|
||||||
|
require.NoError(t, diags.Error())
|
||||||
|
}
|
||||||
|
|
||||||
func TestProcessTargetModeDefault(t *testing.T) {
|
func TestProcessTargetModeDefault(t *testing.T) {
|
||||||
b := mockBundle("")
|
b := mockBundle("")
|
||||||
|
|
||||||
m := ProcessTargetMode()
|
m := bundle.Seq(ProcessTargetMode(), ApplyPresets())
|
||||||
diags := bundle.Apply(context.Background(), b, m)
|
diags := bundle.Apply(context.Background(), b, m)
|
||||||
require.NoError(t, diags.Error())
|
require.NoError(t, diags.Error())
|
||||||
assert.Equal(t, "job1", b.Config.Resources.Jobs["job1"].Name)
|
assert.Equal(t, "job1", b.Config.Resources.Jobs["job1"].Name)
|
||||||
|
@ -307,7 +348,7 @@ func TestAllResourcesMocked(t *testing.T) {
|
||||||
func TestAllResourcesRenamed(t *testing.T) {
|
func TestAllResourcesRenamed(t *testing.T) {
|
||||||
b := mockBundle(config.Development)
|
b := mockBundle(config.Development)
|
||||||
|
|
||||||
m := ProcessTargetMode()
|
m := bundle.Seq(ProcessTargetMode(), ApplyPresets())
|
||||||
diags := bundle.Apply(context.Background(), b, m)
|
diags := bundle.Apply(context.Background(), b, m)
|
||||||
require.NoError(t, diags.Error())
|
require.NoError(t, diags.Error())
|
||||||
|
|
||||||
|
@ -337,8 +378,7 @@ func TestDisableLocking(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
b := mockBundle(config.Development)
|
b := mockBundle(config.Development)
|
||||||
|
|
||||||
err := bundle.Apply(ctx, b, ProcessTargetMode())
|
transformDevelopmentMode(ctx, b)
|
||||||
require.Nil(t, err)
|
|
||||||
assert.False(t, b.Config.Bundle.Deployment.Lock.IsEnabled())
|
assert.False(t, b.Config.Bundle.Deployment.Lock.IsEnabled())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -348,7 +388,97 @@ func TestDisableLockingDisabled(t *testing.T) {
|
||||||
explicitlyEnabled := true
|
explicitlyEnabled := true
|
||||||
b.Config.Bundle.Deployment.Lock.Enabled = &explicitlyEnabled
|
b.Config.Bundle.Deployment.Lock.Enabled = &explicitlyEnabled
|
||||||
|
|
||||||
err := bundle.Apply(ctx, b, ProcessTargetMode())
|
transformDevelopmentMode(ctx, b)
|
||||||
require.Nil(t, err)
|
|
||||||
assert.True(t, b.Config.Bundle.Deployment.Lock.IsEnabled(), "Deployment lock should remain enabled in development mode when explicitly enabled")
|
assert.True(t, b.Config.Bundle.Deployment.Lock.IsEnabled(), "Deployment lock should remain enabled in development mode when explicitly enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPrefixAlreadySet(t *testing.T) {
|
||||||
|
b := mockBundle(config.Development)
|
||||||
|
b.Config.Presets.NamePrefix = "custom_lennart_deploy_"
|
||||||
|
|
||||||
|
m := bundle.Seq(ProcessTargetMode(), ApplyPresets())
|
||||||
|
diags := bundle.Apply(context.Background(), b, m)
|
||||||
|
require.NoError(t, diags.Error())
|
||||||
|
|
||||||
|
assert.Equal(t, "custom_lennart_deploy_job1", b.Config.Resources.Jobs["job1"].Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTagsAlreadySet(t *testing.T) {
|
||||||
|
b := mockBundle(config.Development)
|
||||||
|
b.Config.Presets.Tags = map[string]string{
|
||||||
|
"custom": "tag",
|
||||||
|
"dev": "foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
m := bundle.Seq(ProcessTargetMode(), ApplyPresets())
|
||||||
|
diags := bundle.Apply(context.Background(), b, m)
|
||||||
|
require.NoError(t, diags.Error())
|
||||||
|
|
||||||
|
assert.Equal(t, "tag", b.Config.Resources.Jobs["job1"].Tags["custom"])
|
||||||
|
assert.Equal(t, "foo", b.Config.Resources.Jobs["job1"].Tags["dev"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTagsNil(t *testing.T) {
|
||||||
|
b := mockBundle(config.Development)
|
||||||
|
b.Config.Presets.Tags = nil
|
||||||
|
|
||||||
|
m := bundle.Seq(ProcessTargetMode(), ApplyPresets())
|
||||||
|
diags := bundle.Apply(context.Background(), b, m)
|
||||||
|
require.NoError(t, diags.Error())
|
||||||
|
|
||||||
|
assert.Equal(t, "lennart", b.Config.Resources.Jobs["job2"].Tags["dev"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTagsEmptySet(t *testing.T) {
|
||||||
|
b := mockBundle(config.Development)
|
||||||
|
b.Config.Presets.Tags = map[string]string{}
|
||||||
|
|
||||||
|
m := bundle.Seq(ProcessTargetMode(), ApplyPresets())
|
||||||
|
diags := bundle.Apply(context.Background(), b, m)
|
||||||
|
require.NoError(t, diags.Error())
|
||||||
|
|
||||||
|
assert.Equal(t, "lennart", b.Config.Resources.Jobs["job2"].Tags["dev"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobsMaxConcurrentRunsAlreadySet(t *testing.T) {
|
||||||
|
b := mockBundle(config.Development)
|
||||||
|
b.Config.Presets.JobsMaxConcurrentRuns = 10
|
||||||
|
|
||||||
|
m := bundle.Seq(ProcessTargetMode(), ApplyPresets())
|
||||||
|
diags := bundle.Apply(context.Background(), b, m)
|
||||||
|
require.NoError(t, diags.Error())
|
||||||
|
|
||||||
|
assert.Equal(t, 10, b.Config.Resources.Jobs["job1"].MaxConcurrentRuns)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJobsMaxConcurrentRunsDisabled(t *testing.T) {
|
||||||
|
b := mockBundle(config.Development)
|
||||||
|
b.Config.Presets.JobsMaxConcurrentRuns = 1
|
||||||
|
|
||||||
|
m := bundle.Seq(ProcessTargetMode(), ApplyPresets())
|
||||||
|
diags := bundle.Apply(context.Background(), b, m)
|
||||||
|
require.NoError(t, diags.Error())
|
||||||
|
|
||||||
|
assert.Equal(t, 1, b.Config.Resources.Jobs["job1"].MaxConcurrentRuns)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTriggerPauseStatusWhenUnpaused(t *testing.T) {
|
||||||
|
b := mockBundle(config.Development)
|
||||||
|
b.Config.Presets.TriggerPauseStatus = config.Unpaused
|
||||||
|
|
||||||
|
m := bundle.Seq(ProcessTargetMode(), ApplyPresets())
|
||||||
|
diags := bundle.Apply(context.Background(), b, m)
|
||||||
|
require.ErrorContains(t, diags.Error(), "target with 'mode: development' cannot set trigger pause status to UNPAUSED by default")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPipelinesDevelopmentDisabled(t *testing.T) {
|
||||||
|
b := mockBundle(config.Development)
|
||||||
|
notEnabled := false
|
||||||
|
b.Config.Presets.PipelinesDevelopment = ¬Enabled
|
||||||
|
|
||||||
|
m := bundle.Seq(ProcessTargetMode(), ApplyPresets())
|
||||||
|
diags := bundle.Apply(context.Background(), b, m)
|
||||||
|
require.NoError(t, diags.Error())
|
||||||
|
|
||||||
|
assert.False(t, b.Config.Resources.Pipelines["pipeline1"].PipelineSpec.Development)
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
const Paused = "PAUSED"
|
||||||
|
const Unpaused = "UNPAUSED"
|
||||||
|
|
||||||
|
type Presets struct {
|
||||||
|
// NamePrefix to prepend to all resource names.
|
||||||
|
NamePrefix string `json:"name_prefix,omitempty"`
|
||||||
|
|
||||||
|
// PipelinesDevelopment is the default value for the development field of pipelines.
|
||||||
|
PipelinesDevelopment *bool `json:"pipelines_development,omitempty"`
|
||||||
|
|
||||||
|
// TriggerPauseStatus is the default value for the pause status of all triggers and schedules.
|
||||||
|
// Either config.Paused, config.Unpaused, or empty.
|
||||||
|
TriggerPauseStatus string `json:"trigger_pause_status,omitempty"`
|
||||||
|
|
||||||
|
// JobsMaxConcurrentRuns is the default value for the max concurrent runs of jobs.
|
||||||
|
JobsMaxConcurrentRuns int `json:"jobs_max_concurrent_runs,omitempty"`
|
||||||
|
|
||||||
|
// Tags to add to all resources.
|
||||||
|
Tags map[string]string `json:"tags,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExplicitlyEnabled tests whether this feature is explicitly enabled.
|
||||||
|
func IsExplicitlyEnabled(feature *bool) bool {
|
||||||
|
return feature != nil && *feature
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsExplicitlyDisabled tests whether this feature is explicitly disabled.
|
||||||
|
func IsExplicitlyDisabled(feature *bool) bool {
|
||||||
|
return feature != nil && !*feature
|
||||||
|
}
|
|
@ -60,6 +60,10 @@ type Root struct {
|
||||||
// RunAs section allows to define an execution identity for jobs and pipelines runs
|
// RunAs section allows to define an execution identity for jobs and pipelines runs
|
||||||
RunAs *jobs.JobRunAs `json:"run_as,omitempty"`
|
RunAs *jobs.JobRunAs `json:"run_as,omitempty"`
|
||||||
|
|
||||||
|
// Presets applies preset transformations throughout the bundle, e.g.
|
||||||
|
// adding a name prefix to deployed resources.
|
||||||
|
Presets Presets `json:"presets,omitempty"`
|
||||||
|
|
||||||
Experimental *Experimental `json:"experimental,omitempty"`
|
Experimental *Experimental `json:"experimental,omitempty"`
|
||||||
|
|
||||||
// Permissions section allows to define permissions which will be
|
// Permissions section allows to define permissions which will be
|
||||||
|
@ -307,6 +311,7 @@ func (r *Root) MergeTargetOverrides(name string) error {
|
||||||
"resources",
|
"resources",
|
||||||
"sync",
|
"sync",
|
||||||
"permissions",
|
"permissions",
|
||||||
|
"presets",
|
||||||
} {
|
} {
|
||||||
if root, err = mergeField(root, target, f); err != nil {
|
if root, err = mergeField(root, target, f); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -20,6 +20,10 @@ type Target struct {
|
||||||
// development purposes.
|
// development purposes.
|
||||||
Mode Mode `json:"mode,omitempty"`
|
Mode Mode `json:"mode,omitempty"`
|
||||||
|
|
||||||
|
// Mutator configurations that e.g. change the
|
||||||
|
// name prefix of deployed resources.
|
||||||
|
Presets Presets `json:"presets,omitempty"`
|
||||||
|
|
||||||
// Overrides the compute used for jobs and other supported assets.
|
// Overrides the compute used for jobs and other supported assets.
|
||||||
ComputeID string `json:"compute_id,omitempty"`
|
ComputeID string `json:"compute_id,omitempty"`
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,7 @@ func Initialize() bundle.Mutator {
|
||||||
mutator.SetRunAs(),
|
mutator.SetRunAs(),
|
||||||
mutator.OverrideCompute(),
|
mutator.OverrideCompute(),
|
||||||
mutator.ProcessTargetMode(),
|
mutator.ProcessTargetMode(),
|
||||||
|
mutator.ApplyPresets(),
|
||||||
mutator.DefaultQueueing(),
|
mutator.DefaultQueueing(),
|
||||||
mutator.ExpandPipelineGlobPaths(),
|
mutator.ExpandPipelineGlobPaths(),
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
bundle:
|
||||||
|
name: presets
|
||||||
|
|
||||||
|
presets:
|
||||||
|
tags:
|
||||||
|
prod: true
|
||||||
|
team: finance
|
||||||
|
pipelines_development: true
|
||||||
|
|
||||||
|
targets:
|
||||||
|
dev:
|
||||||
|
presets:
|
||||||
|
name_prefix: "myprefix"
|
||||||
|
pipelines_development: true
|
||||||
|
trigger_pause_status: PAUSED
|
||||||
|
jobs_max_concurrent_runs: 10
|
||||||
|
tags:
|
||||||
|
dev: true
|
||||||
|
prod: false
|
||||||
|
prod:
|
||||||
|
presets:
|
||||||
|
pipelines_development: false
|
|
@ -0,0 +1,28 @@
|
||||||
|
package config_tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/bundle/config"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPresetsDev(t *testing.T) {
|
||||||
|
b := loadTarget(t, "./presets", "dev")
|
||||||
|
|
||||||
|
assert.Equal(t, "myprefix", b.Config.Presets.NamePrefix)
|
||||||
|
assert.Equal(t, config.Paused, b.Config.Presets.TriggerPauseStatus)
|
||||||
|
assert.Equal(t, 10, b.Config.Presets.JobsMaxConcurrentRuns)
|
||||||
|
assert.Equal(t, true, *b.Config.Presets.PipelinesDevelopment)
|
||||||
|
assert.Equal(t, "true", b.Config.Presets.Tags["dev"])
|
||||||
|
assert.Equal(t, "finance", b.Config.Presets.Tags["team"])
|
||||||
|
assert.Equal(t, "false", b.Config.Presets.Tags["prod"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPresetsProd(t *testing.T) {
|
||||||
|
b := loadTarget(t, "./presets", "prod")
|
||||||
|
|
||||||
|
assert.Equal(t, false, *b.Config.Presets.PipelinesDevelopment)
|
||||||
|
assert.Equal(t, "finance", b.Config.Presets.Tags["team"])
|
||||||
|
assert.Equal(t, "true", b.Config.Presets.Tags["prod"])
|
||||||
|
}
|
Loading…
Reference in New Issue