2023-07-12 06:51:54 +00:00
|
|
|
package mutator
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"path"
|
2023-07-30 07:19:49 +00:00
|
|
|
"strings"
|
2023-07-12 06:51:54 +00:00
|
|
|
|
|
|
|
"github.com/databricks/cli/bundle"
|
|
|
|
"github.com/databricks/cli/bundle/config"
|
2023-08-25 09:03:42 +00:00
|
|
|
"github.com/databricks/cli/libs/auth"
|
2024-03-29 13:27:54 +00:00
|
|
|
"github.com/databricks/cli/libs/dyn"
|
2023-08-25 09:03:42 +00:00
|
|
|
"github.com/databricks/cli/libs/log"
|
2023-07-12 06:51:54 +00:00
|
|
|
"github.com/databricks/databricks-sdk-go/service/jobs"
|
|
|
|
"github.com/databricks/databricks-sdk-go/service/ml"
|
|
|
|
)
|
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
type processTargetMode struct{}
|
2023-07-12 06:51:54 +00:00
|
|
|
|
|
|
|
const developmentConcurrentRuns = 4
|
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
func ProcessTargetMode() bundle.Mutator {
|
|
|
|
return &processTargetMode{}
|
2023-07-12 06:51:54 +00:00
|
|
|
}
|
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
func (m *processTargetMode) Name() string {
|
|
|
|
return "ProcessTargetMode"
|
2023-07-12 06:51:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Mark all resources as being for 'development' purposes, i.e.
|
|
|
|
// changing their their name, adding tags, and (in the future)
|
|
|
|
// marking them as 'hidden' in the UI.
|
2024-03-29 13:27:54 +00:00
|
|
|
func transformDevelopmentMode(ctx context.Context, b *bundle.Bundle) error {
|
2023-07-12 06:51:54 +00:00
|
|
|
r := b.Config.Resources
|
|
|
|
|
2023-10-02 06:58:51 +00:00
|
|
|
shortName := b.Config.Workspace.CurrentUser.ShortName
|
|
|
|
prefix := "[dev " + shortName + "] "
|
|
|
|
|
2024-03-29 13:27:54 +00:00
|
|
|
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")
|
|
|
|
err := disableDeploymentLock(b)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-02 06:58:51 +00:00
|
|
|
// Generate a normalized version of the short name that can be used as a tag value.
|
|
|
|
tagValue := b.Tagging.NormalizeValue(shortName)
|
2023-07-30 07:19:49 +00:00
|
|
|
|
2023-07-12 06:51:54 +00:00
|
|
|
for i := range r.Jobs {
|
2023-07-30 07:19:49 +00:00
|
|
|
r.Jobs[i].Name = prefix + r.Jobs[i].Name
|
2023-07-12 06:51:54 +00:00
|
|
|
if r.Jobs[i].Tags == nil {
|
|
|
|
r.Jobs[i].Tags = make(map[string]string)
|
|
|
|
}
|
2023-10-02 06:58:51 +00:00
|
|
|
r.Jobs[i].Tags["dev"] = tagValue
|
2023-07-12 06:51:54 +00:00
|
|
|
if r.Jobs[i].MaxConcurrentRuns == 0 {
|
|
|
|
r.Jobs[i].MaxConcurrentRuns = developmentConcurrentRuns
|
|
|
|
}
|
2023-11-13 19:50:39 +00:00
|
|
|
|
|
|
|
// 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 {
|
2023-07-12 06:51:54 +00:00
|
|
|
r.Jobs[i].Schedule.PauseStatus = jobs.PauseStatusPaused
|
|
|
|
}
|
2023-11-29 16:32:42 +00:00
|
|
|
if r.Jobs[i].Continuous != nil && r.Jobs[i].Continuous.PauseStatus != jobs.PauseStatusUnpaused {
|
2023-07-12 06:51:54 +00:00
|
|
|
r.Jobs[i].Continuous.PauseStatus = jobs.PauseStatusPaused
|
|
|
|
}
|
2023-11-29 16:32:42 +00:00
|
|
|
if r.Jobs[i].Trigger != nil && r.Jobs[i].Trigger.PauseStatus != jobs.PauseStatusUnpaused {
|
2023-07-12 06:51:54 +00:00
|
|
|
r.Jobs[i].Trigger.PauseStatus = jobs.PauseStatusPaused
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := range r.Pipelines {
|
2023-07-30 07:19:49 +00:00
|
|
|
r.Pipelines[i].Name = prefix + r.Pipelines[i].Name
|
2023-07-12 06:51:54 +00:00
|
|
|
r.Pipelines[i].Development = true
|
|
|
|
// (pipelines don't yet support tags)
|
|
|
|
}
|
|
|
|
|
|
|
|
for i := range r.Models {
|
2023-07-30 07:19:49 +00:00
|
|
|
r.Models[i].Name = prefix + r.Models[i].Name
|
2024-02-22 14:52:49 +00:00
|
|
|
r.Models[i].Tags = append(r.Models[i].Tags, ml.ModelTag{Key: "dev", Value: tagValue})
|
2023-07-12 06:51:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for i := range r.Experiments {
|
|
|
|
filepath := r.Experiments[i].Name
|
|
|
|
dir := path.Dir(filepath)
|
|
|
|
base := path.Base(filepath)
|
|
|
|
if dir == "." {
|
2023-07-30 07:19:49 +00:00
|
|
|
r.Experiments[i].Name = prefix + base
|
2023-07-12 06:51:54 +00:00
|
|
|
} else {
|
2023-07-30 07:19:49 +00:00
|
|
|
r.Experiments[i].Name = dir + "/" + prefix + base
|
|
|
|
}
|
2023-10-02 06:58:51 +00:00
|
|
|
r.Experiments[i].Tags = append(r.Experiments[i].Tags, ml.ExperimentTag{Key: "dev", Value: tagValue})
|
2023-07-30 07:19:49 +00:00
|
|
|
}
|
|
|
|
|
2023-09-07 21:54:31 +00:00
|
|
|
for i := range r.ModelServingEndpoints {
|
|
|
|
prefix = "dev_" + b.Config.Workspace.CurrentUser.ShortName + "_"
|
|
|
|
r.ModelServingEndpoints[i].Name = prefix + r.ModelServingEndpoints[i].Name
|
|
|
|
// (model serving doesn't yet support tags)
|
|
|
|
}
|
|
|
|
|
2023-10-16 15:32:49 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2023-07-30 07:19:49 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-03-29 13:27:54 +00:00
|
|
|
func disableDeploymentLock(b *bundle.Bundle) error {
|
|
|
|
return b.Config.Mutate(func(v dyn.Value) (dyn.Value, error) {
|
|
|
|
return dyn.Map(v, "bundle.deployment.lock", func(_ dyn.Path, v dyn.Value) (dyn.Value, error) {
|
|
|
|
return dyn.Set(v, "enabled", dyn.V(false))
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-07-30 07:19:49 +00:00
|
|
|
func validateDevelopmentMode(b *bundle.Bundle) error {
|
2024-01-02 19:58:24 +00:00
|
|
|
if path := findNonUserPath(b); path != "" {
|
2023-07-30 07:19:49 +00:00
|
|
|
return fmt.Errorf("%s must start with '~/' or contain the current username when using 'mode: development'", path)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-01-02 19:58:24 +00:00
|
|
|
func findNonUserPath(b *bundle.Bundle) string {
|
2023-07-30 07:19:49 +00:00
|
|
|
username := b.Config.Workspace.CurrentUser.UserName
|
|
|
|
|
2024-01-02 19:58:24 +00:00
|
|
|
if b.Config.Workspace.RootPath != "" && !strings.Contains(b.Config.Workspace.RootPath, username) {
|
2023-07-30 07:19:49 +00:00
|
|
|
return "root_path"
|
|
|
|
}
|
2024-01-02 19:58:24 +00:00
|
|
|
if b.Config.Workspace.StatePath != "" && !strings.Contains(b.Config.Workspace.StatePath, username) {
|
2023-07-30 07:19:49 +00:00
|
|
|
return "state_path"
|
|
|
|
}
|
2024-01-02 19:58:24 +00:00
|
|
|
if b.Config.Workspace.FilePath != "" && !strings.Contains(b.Config.Workspace.FilePath, username) {
|
2023-11-15 13:37:26 +00:00
|
|
|
return "file_path"
|
2023-07-30 07:19:49 +00:00
|
|
|
}
|
2024-01-02 19:58:24 +00:00
|
|
|
if b.Config.Workspace.ArtifactPath != "" && !strings.Contains(b.Config.Workspace.ArtifactPath, username) {
|
2023-11-15 13:37:26 +00:00
|
|
|
return "artifact_path"
|
2023-07-30 07:19:49 +00:00
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func validateProductionMode(ctx context.Context, b *bundle.Bundle, isPrincipalUsed bool) error {
|
2023-07-30 12:44:33 +00:00
|
|
|
if b.Config.Bundle.Git.Inferred {
|
2023-08-17 15:22:32 +00:00
|
|
|
env := b.Config.Bundle.Target
|
2023-08-25 09:03:42 +00:00
|
|
|
log.Warnf(ctx, "target with 'mode: production' should specify an explicit 'targets.%s.git' configuration", env)
|
2023-07-30 12:44:33 +00:00
|
|
|
}
|
|
|
|
|
2023-07-30 07:19:49 +00:00
|
|
|
r := b.Config.Resources
|
|
|
|
for i := range r.Pipelines {
|
|
|
|
if r.Pipelines[i].Development {
|
2024-01-02 19:58:24 +00:00
|
|
|
return fmt.Errorf("target with 'mode: production' cannot include a pipeline with 'development: true'")
|
2023-07-12 06:51:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-02 19:58:24 +00:00
|
|
|
if !isPrincipalUsed && !isRunAsSet(r) {
|
|
|
|
return fmt.Errorf("'run_as' must be set for all jobs when using 'mode: production'")
|
2023-07-30 07:19:49 +00:00
|
|
|
}
|
2023-07-12 06:51:54 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-07-30 07:19:49 +00:00
|
|
|
// Determines whether run_as is explicitly set for all resources.
|
|
|
|
// We do this in a best-effort fashion rather than check the top-level
|
|
|
|
// 'run_as' field because the latter is not required to be set.
|
|
|
|
func isRunAsSet(r config.Resources) bool {
|
|
|
|
for i := range r.Jobs {
|
|
|
|
if r.Jobs[i].RunAs == nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2023-08-17 15:22:32 +00:00
|
|
|
func (m *processTargetMode) Apply(ctx context.Context, b *bundle.Bundle) error {
|
2023-07-12 06:51:54 +00:00
|
|
|
switch b.Config.Bundle.Mode {
|
|
|
|
case config.Development:
|
2023-07-30 07:19:49 +00:00
|
|
|
err := validateDevelopmentMode(b)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return transformDevelopmentMode(b)
|
|
|
|
case config.Production:
|
2023-10-26 14:58:16 +00:00
|
|
|
isPrincipal := auth.IsServicePrincipal(b.Config.Workspace.CurrentUser.UserName)
|
2023-07-30 07:19:49 +00:00
|
|
|
return validateProductionMode(ctx, b, isPrincipal)
|
2023-07-12 06:51:54 +00:00
|
|
|
case "":
|
|
|
|
// No action
|
|
|
|
default:
|
2023-08-21 09:09:08 +00:00
|
|
|
return fmt.Errorf("unsupported value '%s' specified for 'mode': must be either 'development' or 'production'", b.Config.Bundle.Mode)
|
2023-07-12 06:51:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|