github.com/databricks/cli@v0.203.0/bundle/config/mutator/process_environment_mode.go (about) 1 package mutator 2 3 import ( 4 "context" 5 "fmt" 6 "path" 7 "strings" 8 9 "github.com/databricks/cli/bundle" 10 "github.com/databricks/cli/bundle/config" 11 "github.com/databricks/databricks-sdk-go/service/iam" 12 "github.com/databricks/databricks-sdk-go/service/jobs" 13 "github.com/databricks/databricks-sdk-go/service/ml" 14 ) 15 16 type processEnvironmentMode struct{} 17 18 const developmentConcurrentRuns = 4 19 20 func ProcessEnvironmentMode() bundle.Mutator { 21 return &processEnvironmentMode{} 22 } 23 24 func (m *processEnvironmentMode) Name() string { 25 return "ProcessEnvironmentMode" 26 } 27 28 // Mark all resources as being for 'development' purposes, i.e. 29 // changing their their name, adding tags, and (in the future) 30 // marking them as 'hidden' in the UI. 31 func transformDevelopmentMode(b *bundle.Bundle) error { 32 r := b.Config.Resources 33 34 prefix := "[dev " + b.Config.Workspace.CurrentUser.ShortName + "] " 35 36 for i := range r.Jobs { 37 r.Jobs[i].Name = prefix + r.Jobs[i].Name 38 if r.Jobs[i].Tags == nil { 39 r.Jobs[i].Tags = make(map[string]string) 40 } 41 r.Jobs[i].Tags["dev"] = b.Config.Workspace.CurrentUser.DisplayName 42 if r.Jobs[i].MaxConcurrentRuns == 0 { 43 r.Jobs[i].MaxConcurrentRuns = developmentConcurrentRuns 44 } 45 if r.Jobs[i].Schedule != nil { 46 r.Jobs[i].Schedule.PauseStatus = jobs.PauseStatusPaused 47 } 48 if r.Jobs[i].Continuous != nil { 49 r.Jobs[i].Continuous.PauseStatus = jobs.PauseStatusPaused 50 } 51 if r.Jobs[i].Trigger != nil { 52 r.Jobs[i].Trigger.PauseStatus = jobs.PauseStatusPaused 53 } 54 } 55 56 for i := range r.Pipelines { 57 r.Pipelines[i].Name = prefix + r.Pipelines[i].Name 58 r.Pipelines[i].Development = true 59 // (pipelines don't yet support tags) 60 } 61 62 for i := range r.Models { 63 r.Models[i].Name = prefix + r.Models[i].Name 64 r.Models[i].Tags = append(r.Models[i].Tags, ml.ModelTag{Key: "dev", Value: ""}) 65 } 66 67 for i := range r.Experiments { 68 filepath := r.Experiments[i].Name 69 dir := path.Dir(filepath) 70 base := path.Base(filepath) 71 if dir == "." { 72 r.Experiments[i].Name = prefix + base 73 } else { 74 r.Experiments[i].Name = dir + "/" + prefix + base 75 } 76 r.Experiments[i].Tags = append(r.Experiments[i].Tags, ml.ExperimentTag{Key: "dev", Value: b.Config.Workspace.CurrentUser.DisplayName}) 77 } 78 79 return nil 80 } 81 82 func validateDevelopmentMode(b *bundle.Bundle) error { 83 if path := findIncorrectPath(b, config.Development); path != "" { 84 return fmt.Errorf("%s must start with '~/' or contain the current username when using 'mode: development'", path) 85 } 86 return nil 87 } 88 89 func findIncorrectPath(b *bundle.Bundle, mode config.Mode) string { 90 username := b.Config.Workspace.CurrentUser.UserName 91 containsExpected := true 92 if mode == config.Production { 93 containsExpected = false 94 } 95 96 if strings.Contains(b.Config.Workspace.RootPath, username) != containsExpected && b.Config.Workspace.RootPath != "" { 97 return "root_path" 98 } 99 if strings.Contains(b.Config.Workspace.StatePath, username) != containsExpected { 100 return "state_path" 101 } 102 if strings.Contains(b.Config.Workspace.FilesPath, username) != containsExpected { 103 return "files_path" 104 } 105 if strings.Contains(b.Config.Workspace.ArtifactsPath, username) != containsExpected { 106 return "artifacts_path" 107 } 108 return "" 109 } 110 111 func validateProductionMode(ctx context.Context, b *bundle.Bundle, isPrincipalUsed bool) error { 112 if b.Config.Bundle.Git.Inferred { 113 env := b.Config.Bundle.Environment 114 return fmt.Errorf("environment with 'mode: production' must specify an explicit 'environments.%s.git' configuration", env) 115 } 116 117 r := b.Config.Resources 118 for i := range r.Pipelines { 119 if r.Pipelines[i].Development { 120 return fmt.Errorf("environment with 'mode: production' cannot specify a pipeline with 'development: true'") 121 } 122 } 123 124 if !isPrincipalUsed { 125 if path := findIncorrectPath(b, config.Production); path != "" { 126 message := "%s must not contain the current username when using 'mode: production'" 127 if path == "root_path" { 128 return fmt.Errorf(message+"\n tip: set workspace.root_path to a shared path such as /Shared/.bundle/${bundle.name}/${bundle.environment}", path) 129 } else { 130 return fmt.Errorf(message, path) 131 } 132 } 133 134 if !isRunAsSet(r) { 135 return fmt.Errorf("'run_as' must be set for all jobs when using 'mode: production'") 136 } 137 } 138 return nil 139 } 140 141 // Determines whether a service principal identity is used to run the CLI. 142 func isServicePrincipalUsed(ctx context.Context, b *bundle.Bundle) (bool, error) { 143 ws := b.WorkspaceClient() 144 145 // Check if a principal with the current user's ID exists. 146 // We need to use the ListAll method since Get is only usable by admins. 147 matches, err := ws.ServicePrincipals.ListAll(ctx, iam.ListServicePrincipalsRequest{ 148 Filter: "id eq " + b.Config.Workspace.CurrentUser.Id, 149 }) 150 if err != nil { 151 return false, err 152 } 153 return len(matches) > 0, nil 154 } 155 156 // Determines whether run_as is explicitly set for all resources. 157 // We do this in a best-effort fashion rather than check the top-level 158 // 'run_as' field because the latter is not required to be set. 159 func isRunAsSet(r config.Resources) bool { 160 for i := range r.Jobs { 161 if r.Jobs[i].RunAs == nil { 162 return false 163 } 164 } 165 return true 166 } 167 168 func (m *processEnvironmentMode) Apply(ctx context.Context, b *bundle.Bundle) error { 169 switch b.Config.Bundle.Mode { 170 case config.Development: 171 err := validateDevelopmentMode(b) 172 if err != nil { 173 return err 174 } 175 return transformDevelopmentMode(b) 176 case config.Production: 177 isPrincipal, err := isServicePrincipalUsed(ctx, b) 178 if err != nil { 179 return err 180 } 181 return validateProductionMode(ctx, b, isPrincipal) 182 case "": 183 // No action 184 default: 185 return fmt.Errorf("unsupported value specified for 'mode': %s", b.Config.Bundle.Mode) 186 } 187 188 return nil 189 }