2023-07-12 06:51:54 +00:00
package mutator
import (
"context"
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-25 14:18:47 +00:00
"github.com/databricks/cli/libs/diag"
2024-08-19 18:18:50 +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
)
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-08-19 18:18:50 +00:00
func transformDevelopmentMode ( ctx context . Context , b * bundle . Bundle ) {
2024-04-18 01:59:39 +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" )
2024-06-21 11:14:33 +00:00
disabled := false
b . Config . Bundle . Deployment . Lock . Enabled = & disabled
2024-04-18 01:59:39 +00:00
}
2023-07-12 06:51:54 +00:00
2024-08-19 18:18:50 +00:00
t := & b . Config . Presets
2023-10-02 06:58:51 +00:00
shortName := b . Config . Workspace . CurrentUser . ShortName
2023-07-12 06:51:54 +00:00
2024-08-19 18:18:50 +00:00
if t . NamePrefix == "" {
t . NamePrefix = "[dev " + shortName + "] "
2023-07-12 06:51:54 +00:00
}
2024-08-19 18:18:50 +00:00
if t . Tags == nil {
t . Tags = map [ string ] string { }
2023-07-30 07:19:49 +00:00
}
2024-08-19 18:18:50 +00:00
_ , exists := t . Tags [ "dev" ]
if ! exists {
t . Tags [ "dev" ] = b . Tagging . NormalizeValue ( shortName )
2023-09-07 21:54:31 +00:00
}
2024-08-19 18:18:50 +00:00
if t . JobsMaxConcurrentRuns == 0 {
t . JobsMaxConcurrentRuns = developmentConcurrentRuns
2023-10-16 15:32:49 +00:00
}
2024-08-19 18:18:50 +00:00
if t . TriggerPauseStatus == "" {
t . TriggerPauseStatus = config . Paused
2024-06-19 13:54:35 +00:00
}
2024-08-19 18:18:50 +00:00
if ! config . IsExplicitlyDisabled ( t . PipelinesDevelopment ) {
enabled := true
t . PipelinesDevelopment = & enabled
2024-07-31 12:16:28 +00:00
}
2023-07-30 07:19:49 +00:00
}
2024-03-25 14:18:47 +00:00
func validateDevelopmentMode ( b * bundle . Bundle ) diag . Diagnostics {
2024-08-28 12:14:19 +00:00
var diags diag . Diagnostics
2024-08-19 18:18:50 +00:00
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 {
2024-08-28 12:14:19 +00:00
diags = diags . Append ( diag . Diagnostic {
2024-08-19 18:18:50 +00:00
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" ) } ,
2024-08-28 12:14:19 +00:00
} )
2024-08-19 18:18:50 +00:00
}
// Make sure this development copy has unique names and paths to avoid conflicts
2024-01-02 19:58:24 +00:00
if path := findNonUserPath ( b ) ; path != "" {
2024-08-28 12:14:19 +00:00
if path == "artifact_path" && strings . HasPrefix ( b . Config . Workspace . ArtifactPath , "/Volumes" ) {
// For Volumes paths we recommend including the current username as a substring
diags = diags . Extend ( diag . Errorf ( "%s should contain the current username or ${workspace.current_user.short_name} to ensure uniqueness when using 'mode: development'" , path ) )
} else {
// For non-Volumes paths recommend simply putting things in the home folder
diags = diags . Extend ( diag . Errorf ( "%s must start with '~/' or contain the current username to ensure uniqueness when using 'mode: development'" , path ) )
}
2023-07-30 07:19:49 +00:00
}
2024-08-19 18:18:50 +00:00
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.
2024-08-28 12:14:19 +00:00
diags = diags . Append ( diag . Diagnostic {
2024-08-19 18:18:50 +00:00
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" ) } ,
2024-08-28 12:14:19 +00:00
} )
2024-08-19 18:18:50 +00:00
}
2024-08-28 12:14:19 +00:00
return diags
2023-07-30 07:19:49 +00:00
}
2024-08-28 12:14:19 +00:00
// findNonUserPath finds the first workspace path such as root_path that doesn't
// contain the current username or current user's shortname.
2024-01-02 19:58:24 +00:00
func findNonUserPath ( b * bundle . Bundle ) string {
2024-08-28 12:14:19 +00:00
containsName := func ( path string ) bool {
username := b . Config . Workspace . CurrentUser . UserName
shortname := b . Config . Workspace . CurrentUser . ShortName
return strings . Contains ( path , username ) || strings . Contains ( path , shortname )
}
2023-07-30 07:19:49 +00:00
2024-08-28 12:14:19 +00:00
if b . Config . Workspace . RootPath != "" && ! containsName ( b . Config . Workspace . RootPath ) {
2023-07-30 07:19:49 +00:00
return "root_path"
}
2024-08-28 12:14:19 +00:00
if b . Config . Workspace . FilePath != "" && ! containsName ( b . Config . Workspace . FilePath ) {
2023-11-15 13:37:26 +00:00
return "file_path"
2023-07-30 07:19:49 +00:00
}
2024-10-02 13:55:40 +00:00
if b . Config . Workspace . ResourcePath != "" && ! containsName ( b . Config . Workspace . ResourcePath ) {
return "resource_path"
}
2024-08-28 12:14:19 +00:00
if b . Config . Workspace . ArtifactPath != "" && ! containsName ( b . Config . Workspace . ArtifactPath ) {
2023-11-15 13:37:26 +00:00
return "artifact_path"
2023-07-30 07:19:49 +00:00
}
2024-10-02 13:55:40 +00:00
if b . Config . Workspace . StatePath != "" && ! containsName ( b . Config . Workspace . StatePath ) {
return "state_path"
}
2023-07-30 07:19:49 +00:00
return ""
}
2024-03-25 14:18:47 +00:00
func validateProductionMode ( ctx context . Context , b * bundle . Bundle , isPrincipalUsed bool ) diag . Diagnostics {
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-03-25 14:18:47 +00:00
return diag . 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 ) {
2024-03-25 14:18:47 +00:00
return diag . 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
}
2024-03-25 14:18:47 +00:00
func ( m * processTargetMode ) Apply ( ctx context . Context , b * bundle . Bundle ) diag . Diagnostics {
2023-07-12 06:51:54 +00:00
switch b . Config . Bundle . Mode {
case config . Development :
2024-03-25 14:18:47 +00:00
diags := validateDevelopmentMode ( b )
2024-08-19 18:18:50 +00:00
if diags . HasError ( ) {
2024-03-25 14:18:47 +00:00
return diags
2023-07-30 07:19:49 +00:00
}
2024-08-19 18:18:50 +00:00
transformDevelopmentMode ( ctx , b )
return diags
2023-07-30 07:19:49 +00:00
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 :
2024-03-25 14:18:47 +00:00
return diag . 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
}