2023-08-23 16:47:07 +00:00
package mutator
import (
"context"
2024-03-27 16:13:53 +00:00
"fmt"
2024-04-22 11:51:41 +00:00
"slices"
2023-08-23 16:47:07 +00:00
"github.com/databricks/cli/bundle"
2024-04-22 11:51:41 +00:00
"github.com/databricks/cli/bundle/config/resources"
2024-03-25 14:18:47 +00:00
"github.com/databricks/cli/libs/diag"
2024-03-27 16:13:53 +00:00
"github.com/databricks/cli/libs/dyn"
2023-08-23 16:47:07 +00:00
"github.com/databricks/databricks-sdk-go/service/jobs"
)
type setRunAs struct {
}
2024-03-27 16:13:53 +00:00
// This mutator does two things:
//
// 1. Sets the run_as field for jobs to the value of the run_as field in the bundle.
//
// 2. Validates that the bundle run_as configuration is valid in the context of the bundle.
// If the run_as user is different from the current deployment user, DABs only
// supports a subset of resources.
2023-08-23 16:47:07 +00:00
func SetRunAs ( ) bundle . Mutator {
return & setRunAs { }
}
func ( m * setRunAs ) Name ( ) string {
return "SetRunAs"
}
2024-10-10 11:18:23 +00:00
func reportRunAsNotSupported ( resourceType string , location dyn . Location , currentUser string , runAsUser string ) diag . Diagnostics {
return diag . Diagnostics { {
Summary : fmt . Sprintf ( "%s do not support a setting a run_as user that is different from the owner.\n" +
"Current identity: %s. Run as identity: %s.\n" +
"See https://docs.databricks.com/dev-tools/bundles/run-as.html to learn more about the run_as property." , resourceType , currentUser , runAsUser ) ,
Locations : [ ] dyn . Location { location } ,
Severity : diag . Error ,
} }
2024-03-27 16:13:53 +00:00
}
2024-10-10 11:18:23 +00:00
func validateRunAs ( b * bundle . Bundle ) diag . Diagnostics {
diags := diag . Diagnostics { }
2024-03-27 16:13:53 +00:00
2024-10-10 11:18:23 +00:00
neitherSpecifiedErr := diag . Diagnostics { {
Summary : "run_as section must specify exactly one identity. Neither service_principal_name nor user_name is specified" ,
Locations : [ ] dyn . Location { b . Config . GetLocation ( "run_as" ) } ,
Severity : diag . Error ,
} }
2024-03-27 16:13:53 +00:00
2024-10-10 11:18:23 +00:00
// Fail fast if neither service_principal_name nor user_name are specified, but the
2024-06-27 13:28:19 +00:00
// run_as section is present.
if b . Config . Value ( ) . Get ( "run_as" ) . Kind ( ) == dyn . KindNil {
return neitherSpecifiedErr
}
2024-10-10 11:18:23 +00:00
// Fail fast if one or both of service_principal_name and user_name are specified,
2024-06-27 13:28:19 +00:00
// but with empty values.
2024-10-10 11:18:23 +00:00
runAs := b . Config . RunAs
if runAs . ServicePrincipalName == "" && runAs . UserName == "" {
2024-06-27 13:28:19 +00:00
return neitherSpecifiedErr
2024-03-27 16:13:53 +00:00
}
if runAs . UserName != "" && runAs . ServicePrincipalName != "" {
2024-10-10 11:18:23 +00:00
diags = diags . Extend ( diag . Diagnostics { {
Summary : "run_as section cannot specify both user_name and service_principal_name" ,
Locations : [ ] dyn . Location { b . Config . GetLocation ( "run_as" ) } ,
Severity : diag . Error ,
} } )
2024-03-27 16:13:53 +00:00
}
identity := runAs . ServicePrincipalName
if identity == "" {
identity = runAs . UserName
}
// All resources are supported if the run_as identity is the same as the current deployment identity.
if identity == b . Config . Workspace . CurrentUser . UserName {
2024-10-10 11:18:23 +00:00
return diags
2024-03-27 16:13:53 +00:00
}
// DLT pipelines do not support run_as in the API.
if len ( b . Config . Resources . Pipelines ) > 0 {
2024-10-10 11:18:23 +00:00
diags = diags . Extend ( reportRunAsNotSupported (
"pipelines" ,
b . Config . GetLocation ( "resources.pipelines" ) ,
b . Config . Workspace . CurrentUser . UserName ,
identity ,
) )
2024-03-27 16:13:53 +00:00
}
// Model serving endpoints do not support run_as in the API.
if len ( b . Config . Resources . ModelServingEndpoints ) > 0 {
2024-10-10 11:18:23 +00:00
diags = diags . Extend ( reportRunAsNotSupported (
"model_serving_endpoints" ,
b . Config . GetLocation ( "resources.model_serving_endpoints" ) ,
b . Config . Workspace . CurrentUser . UserName ,
identity ,
) )
2024-03-27 16:13:53 +00:00
}
2024-05-31 09:42:25 +00:00
// Monitors do not support run_as in the API.
if len ( b . Config . Resources . QualityMonitors ) > 0 {
2024-10-10 11:18:23 +00:00
diags = diags . Extend ( reportRunAsNotSupported (
"quality_monitors" ,
b . Config . GetLocation ( "resources.quality_monitors" ) ,
b . Config . Workspace . CurrentUser . UserName ,
identity ,
) )
2024-05-31 09:42:25 +00:00
}
2024-10-10 11:18:23 +00:00
return diags
2024-03-27 16:13:53 +00:00
}
2024-04-22 11:51:41 +00:00
func setRunAsForJobs ( b * bundle . Bundle ) {
2023-08-23 16:47:07 +00:00
runAs := b . Config . RunAs
if runAs == nil {
2024-04-22 11:51:41 +00:00
return
2023-08-23 16:47:07 +00:00
}
for i := range b . Config . Resources . Jobs {
job := b . Config . Resources . Jobs [ i ]
if job . RunAs != nil {
continue
}
job . RunAs = & jobs . JobRunAs {
ServicePrincipalName : runAs . ServicePrincipalName ,
UserName : runAs . UserName ,
}
}
2024-04-22 11:51:41 +00:00
}
// Legacy behavior of run_as for DLT pipelines. Available under the experimental.use_run_as_legacy flag.
// Only available to unblock customers stuck due to breaking changes in https://github.com/databricks/cli/pull/1233
func setPipelineOwnersToRunAsIdentity ( b * bundle . Bundle ) {
runAs := b . Config . RunAs
if runAs == nil {
return
}
me := b . Config . Workspace . CurrentUser . UserName
// If user deploying the bundle and the one defined in run_as are the same
// Do not add IS_OWNER permission. Current user is implied to be an owner in this case.
// Otherwise, it will fail due to this bug https://github.com/databricks/terraform-provider-databricks/issues/2407
if runAs . UserName == me || runAs . ServicePrincipalName == me {
return
}
for i := range b . Config . Resources . Pipelines {
pipeline := b . Config . Resources . Pipelines [ i ]
pipeline . Permissions = slices . DeleteFunc ( pipeline . Permissions , func ( p resources . Permission ) bool {
return ( runAs . ServicePrincipalName != "" && p . ServicePrincipalName == runAs . ServicePrincipalName ) ||
( runAs . UserName != "" && p . UserName == runAs . UserName )
} )
pipeline . Permissions = append ( pipeline . Permissions , resources . Permission {
Level : "IS_OWNER" ,
ServicePrincipalName : runAs . ServicePrincipalName ,
UserName : runAs . UserName ,
} )
}
}
func ( m * setRunAs ) Apply ( _ context . Context , b * bundle . Bundle ) diag . Diagnostics {
// Mutator is a no-op if run_as is not specified in the bundle
2024-06-27 13:28:19 +00:00
if b . Config . Value ( ) . Get ( "run_as" ) . Kind ( ) == dyn . KindInvalid {
2024-04-22 11:51:41 +00:00
return nil
}
if b . Config . Experimental != nil && b . Config . Experimental . UseLegacyRunAs {
setPipelineOwnersToRunAsIdentity ( b )
setRunAsForJobs ( b )
return diag . Diagnostics {
{
2024-07-23 17:20:11 +00:00
Severity : diag . Warning ,
Summary : "You are using the legacy mode of run_as. The support for this mode is experimental and might be removed in a future release of the CLI. In order to run the DLT pipelines in your DAB as the run_as user this mode changes the owners of the pipelines to the run_as identity, which requires the user deploying the bundle to be a workspace admin, and also a Metastore admin if the pipeline target is in UC." ,
2024-07-25 15:16:27 +00:00
Paths : [ ] dyn . Path { dyn . MustPathFromString ( "experimental.use_legacy_run_as" ) } ,
2024-07-23 17:20:11 +00:00
Locations : b . Config . GetLocations ( "experimental.use_legacy_run_as" ) ,
2024-04-22 11:51:41 +00:00
} ,
}
}
// Assert the run_as configuration is valid in the context of the bundle
2024-10-10 11:18:23 +00:00
diags := validateRunAs ( b )
if diags . HasError ( ) {
return diags
2024-04-22 11:51:41 +00:00
}
2023-08-23 16:47:07 +00:00
2024-04-22 11:51:41 +00:00
setRunAsForJobs ( b )
2023-08-23 16:47:07 +00:00
return nil
}