2023-06-18 14:47:01 +00:00
package mutator
import (
"context"
"fmt"
2023-07-07 16:04:28 +00:00
"path"
2023-07-10 07:12:50 +00:00
"strings"
2023-06-18 14:47:01 +00:00
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
2023-07-10 07:12:50 +00:00
"github.com/databricks/databricks-sdk-go/apierr"
"github.com/databricks/databricks-sdk-go/service/iam"
2023-07-07 09:12:14 +00:00
"github.com/databricks/databricks-sdk-go/service/jobs"
2023-06-18 14:47:01 +00:00
"github.com/databricks/databricks-sdk-go/service/ml"
)
2023-07-10 07:12:50 +00:00
type processEnvironmentMode struct {
// getPrincipalGetByIdImpl overrides the GetPrincipalGetById implementation for testing purposes.
getPrincipalGetByIdImpl func ( ctx context . Context , id string ) ( * iam . ServicePrincipal , error )
}
2023-06-18 14:47:01 +00:00
2023-07-07 09:09:09 +00:00
const developmentConcurrentRuns = 4
2023-06-18 15:40:30 +00:00
2023-07-10 07:12:50 +00:00
func ProcessEnvironmentMode ( ) * processEnvironmentMode {
2023-06-18 14:47:01 +00:00
return & processEnvironmentMode { }
}
func ( m * processEnvironmentMode ) Name ( ) string {
return "ProcessEnvironmentMode"
}
2023-07-03 14:30:42 +00:00
// Mark all resources as being for 'development' purposes, i.e.
2023-06-18 14:47:01 +00:00
// changing their their name, adding tags, and (in the future)
// marking them as 'hidden' in the UI.
2023-07-10 07:12:50 +00:00
func transformDevelopmentMode ( b * bundle . Bundle ) error {
2023-06-18 14:47:01 +00:00
r := b . Config . Resources
2023-07-10 07:12:50 +00:00
prefix := "[dev " + b . Config . Workspace . CurrentUser . ShortName + "] "
2023-06-18 14:47:01 +00:00
for i := range r . Jobs {
2023-07-10 07:12:50 +00:00
r . Jobs [ i ] . Name = prefix + r . Jobs [ i ] . Name
2023-06-18 14:47:01 +00:00
if r . Jobs [ i ] . Tags == nil {
r . Jobs [ i ] . Tags = make ( map [ string ] string )
}
2023-07-10 07:12:50 +00:00
r . Jobs [ i ] . Tags [ "dev" ] = b . Config . Workspace . CurrentUser . DisplayName
2023-06-18 15:40:30 +00:00
if r . Jobs [ i ] . MaxConcurrentRuns == 0 {
2023-07-07 09:09:09 +00:00
r . Jobs [ i ] . MaxConcurrentRuns = developmentConcurrentRuns
2023-06-18 15:40:30 +00:00
}
2023-06-20 09:21:33 +00:00
if r . Jobs [ i ] . Schedule != nil {
2023-07-07 09:12:14 +00:00
r . Jobs [ i ] . Schedule . PauseStatus = jobs . PauseStatusPaused
2023-06-20 09:21:33 +00:00
}
if r . Jobs [ i ] . Continuous != nil {
2023-07-07 09:12:14 +00:00
r . Jobs [ i ] . Continuous . PauseStatus = jobs . PauseStatusPaused
2023-06-20 09:21:33 +00:00
}
if r . Jobs [ i ] . Trigger != nil {
2023-07-07 09:12:14 +00:00
r . Jobs [ i ] . Trigger . PauseStatus = jobs . PauseStatusPaused
2023-06-20 09:21:33 +00:00
}
2023-06-18 14:47:01 +00:00
}
for i := range r . Pipelines {
2023-07-10 07:12:50 +00:00
r . Pipelines [ i ] . Name = prefix + r . Pipelines [ i ] . Name
2023-06-18 14:47:01 +00:00
r . Pipelines [ i ] . Development = true
2023-06-18 15:40:30 +00:00
// (pipelines don't yet support tags)
2023-06-18 14:47:01 +00:00
}
for i := range r . Models {
2023-07-10 07:12:50 +00:00
r . Models [ i ] . Name = prefix + r . Models [ i ] . Name
2023-07-03 14:30:42 +00:00
r . Models [ i ] . Tags = append ( r . Models [ i ] . Tags , ml . ModelTag { Key : "dev" , Value : "" } )
2023-06-18 14:47:01 +00:00
}
for i := range r . Experiments {
2023-07-07 16:04:28 +00:00
filepath := r . Experiments [ i ] . Name
dir := path . Dir ( filepath )
base := path . Base ( filepath )
2023-07-07 09:49:02 +00:00
if dir == "." {
2023-07-10 07:12:50 +00:00
r . Experiments [ i ] . Name = prefix + base
2023-07-07 09:49:02 +00:00
} else {
2023-07-10 07:12:50 +00:00
r . Experiments [ i ] . Name = dir + "/" + prefix + base
2023-07-07 09:49:02 +00:00
}
2023-07-10 07:12:50 +00:00
r . Experiments [ i ] . Tags = append ( r . Experiments [ i ] . Tags , ml . ExperimentTag { Key : "dev" , Value : b . Config . Workspace . CurrentUser . DisplayName } )
2023-06-18 14:47:01 +00:00
}
return nil
}
2023-07-10 07:12:50 +00:00
func validateDevelopmentMode ( b * bundle . Bundle ) error {
if isUserSpecificDeployment ( b ) {
return fmt . Errorf ( "environment with 'mode: development' must deploy to a location specific to the user, and should e.g. set 'root_path: ~/.bundle/${bundle.name}/${bundle.environment}'" )
}
return nil
}
func isUserSpecificDeployment ( b * bundle . Bundle ) bool {
username := b . Config . Workspace . CurrentUser . UserName
return ! strings . Contains ( b . Config . Workspace . StatePath , username ) ||
! strings . Contains ( b . Config . Workspace . ArtifactsPath , username ) ||
! strings . Contains ( b . Config . Workspace . FilesPath , username )
}
func ( m * processEnvironmentMode ) validateProductionMode ( ctx context . Context , b * bundle . Bundle ) error {
if b . Config . Bundle . Git . Inferred {
TODO : show a nice human error here ? : (
return fmt . Errorf ( "environment with 'mode: production' must specify an explicit 'git' configuration" )
}
r := b . Config . Resources
for i := range r . Pipelines {
if r . Pipelines [ i ] . Development {
return fmt . Errorf ( "environment with 'mode: production' cannot specify a pipeline with 'development: true'" )
}
}
isPrincipal , err := m . isServicePrincipalUsed ( ctx , b )
if err != nil {
return err
}
if ! isPrincipal {
if isUserSpecificDeployment ( b ) {
return fmt . Errorf ( "environment with 'mode: development' must deploy to a location specific to the user, and should e.g. set 'root_path: ~/.bundle/${bundle.name}/${bundle.environment}'" )
}
if ! arePermissionsSetExplicitly ( r ) {
return fmt . Errorf ( "environment with 'mode: production' must set permissions and run_as for all resources (when not using service principals)" )
}
}
return nil
}
// Determines whether a service principal identity is used to run the CLI.
func ( m * processEnvironmentMode ) isServicePrincipalUsed ( ctx context . Context , b * bundle . Bundle ) ( bool , error ) {
ws := b . WorkspaceClient ( )
getPrincipalById := m . getPrincipalGetByIdImpl
if getPrincipalById == nil {
getPrincipalById = ws . ServicePrincipals . GetById
}
_ , err := getPrincipalById ( ctx , b . Config . Workspace . CurrentUser . Id )
if err != nil {
apiError , ok := err . ( * apierr . APIError )
if ok && apiError . StatusCode == 404 {
return false , nil
}
return false , err
}
return false , nil
}
// Determines whether permissions and run_as are explicitly set for all resources.
// We do this in a best-effort fashion; we may not actually test all resources,
// as we expect customers to use the top-level 'permissions' and 'run_as' fields.
// We'd rather not check for those specific fields though, as customers might
// set specific permissions instead!
func arePermissionsSetExplicitly ( r config . Resources ) bool {
for i := range r . Pipelines {
if r . Pipelines [ i ] . Permissions == nil {
return false
}
}
for i := range r . Jobs {
if r . Jobs [ i ] . Permissions == nil {
return false
}
if r . Jobs [ i ] . RunAs == nil {
return false
}
}
return false
}
2023-06-18 14:47:01 +00:00
func ( m * processEnvironmentMode ) Apply ( ctx context . Context , b * bundle . Bundle ) error {
switch b . Config . Bundle . Mode {
2023-07-03 14:30:42 +00:00
case config . Development :
2023-07-10 07:12:50 +00:00
err := validateDevelopmentMode ( b )
if err != nil {
return err
}
return transformDevelopmentMode ( b )
case config . Production :
return m . validateProductionMode ( ctx , b )
2023-07-03 14:30:42 +00:00
case "" :
2023-06-18 14:47:01 +00:00
// No action
default :
return fmt . Errorf ( "unsupported value specified for 'mode': %s" , b . Config . Bundle . Mode )
}
return nil
}