diff --git a/bundle/config/resources/job.go b/bundle/config/resources/job.go index 17831de4a..124bcb9c2 100644 --- a/bundle/config/resources/job.go +++ b/bundle/config/resources/job.go @@ -3,7 +3,8 @@ package resources import "github.com/databricks/databricks-sdk-go/service/jobs" type Job struct { - ID string `json:"id,omitempty"` + ID string `json:"id,omitempty"` + Permissions []Permission `json:"permissions,omitempty"` *jobs.JobSettings } diff --git a/bundle/config/resources/mlflow_experiment.go b/bundle/config/resources/mlflow_experiment.go index c335821b3..decf3e775 100644 --- a/bundle/config/resources/mlflow_experiment.go +++ b/bundle/config/resources/mlflow_experiment.go @@ -3,5 +3,7 @@ package resources import "github.com/databricks/databricks-sdk-go/service/mlflow" type MlflowExperiment struct { + Permissions []Permission `json:"permissions,omitempty"` + *mlflow.Experiment } diff --git a/bundle/config/resources/mlflow_model.go b/bundle/config/resources/mlflow_model.go index 29354bf71..3e59160eb 100644 --- a/bundle/config/resources/mlflow_model.go +++ b/bundle/config/resources/mlflow_model.go @@ -3,5 +3,7 @@ package resources import "github.com/databricks/databricks-sdk-go/service/mlflow" type MlflowModel struct { + Permissions []Permission `json:"permissions,omitempty"` + *mlflow.RegisteredModel } diff --git a/bundle/config/resources/permission.go b/bundle/config/resources/permission.go new file mode 100644 index 000000000..fa2d8796c --- /dev/null +++ b/bundle/config/resources/permission.go @@ -0,0 +1,11 @@ +package resources + +// Permission holds the permission level setting for a single principal. +// Multiple of these can be defined on any resource. +type Permission struct { + Level string `json:"level"` + + UserName string `json:"user_name,omitempty"` + ServicePrincipalName string `json:"service_principal_name,omitempty"` + GroupName string `json:"group_name,omitempty"` +} diff --git a/bundle/config/resources/pipeline.go b/bundle/config/resources/pipeline.go index ee4004552..caf400b9d 100644 --- a/bundle/config/resources/pipeline.go +++ b/bundle/config/resources/pipeline.go @@ -3,7 +3,8 @@ package resources import "github.com/databricks/databricks-sdk-go/service/pipelines" type Pipeline struct { - ID string `json:"id,omitempty"` + ID string `json:"id,omitempty"` + Permissions []Permission `json:"permissions,omitempty"` *pipelines.PipelineSpec } diff --git a/bundle/deploy/terraform/convert.go b/bundle/deploy/terraform/convert.go index 042d175c9..1b1f92711 100644 --- a/bundle/deploy/terraform/convert.go +++ b/bundle/deploy/terraform/convert.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/databricks/bricks/bundle/config" + "github.com/databricks/bricks/bundle/config/resources" "github.com/databricks/bricks/bundle/internal/tf/schema" tfjson "github.com/hashicorp/terraform-json" ) @@ -14,6 +15,35 @@ func conv(from any, to any) { json.Unmarshal(buf, &to) } +func convPermissions(acl []resources.Permission) *schema.ResourcePermissions { + if len(acl) == 0 { + return nil + } + + resource := schema.ResourcePermissions{} + for _, ac := range acl { + resource.AccessControl = append(resource.AccessControl, convPermission(ac)) + } + + return &resource +} + +func convPermission(ac resources.Permission) schema.ResourcePermissionsAccessControl { + dst := schema.ResourcePermissionsAccessControl{ + PermissionLevel: ac.Level, + } + if ac.UserName != "" { + dst.UserName = ac.UserName + } + if ac.GroupName != "" { + dst.GroupName = ac.GroupName + } + if ac.ServicePrincipalName != "" { + dst.ServicePrincipalName = ac.ServicePrincipalName + } + return dst +} + // BundleToTerraform converts resources in a bundle configuration // to the equivalent Terraform JSON representation. // @@ -29,68 +59,96 @@ func BundleToTerraform(config *config.Root) *schema.Root { var dst schema.ResourceJob conv(src, &dst) - for _, v := range src.Tasks { - var t schema.ResourceJobTask - conv(v, &t) + if src.JobSettings != nil { + for _, v := range src.Tasks { + var t schema.ResourceJobTask + conv(v, &t) - for _, v_ := range v.Libraries { - var l schema.ResourceJobTaskLibrary - conv(v_, &l) - t.Library = append(t.Library, l) + for _, v_ := range v.Libraries { + var l schema.ResourceJobTaskLibrary + conv(v_, &l) + t.Library = append(t.Library, l) + } + + dst.Task = append(dst.Task, t) } - dst.Task = append(dst.Task, t) - } + for _, v := range src.JobClusters { + var t schema.ResourceJobJobCluster + conv(v, &t) + dst.JobCluster = append(dst.JobCluster, t) + } - for _, v := range src.JobClusters { - var t schema.ResourceJobJobCluster - conv(v, &t) - dst.JobCluster = append(dst.JobCluster, t) - } - - // Unblock downstream work. To be addressed more generally later. - if git := src.GitSource; git != nil { - dst.GitSource = &schema.ResourceJobGitSource{ - Url: git.GitUrl, - Branch: git.GitBranch, - Commit: git.GitCommit, - Provider: string(git.GitProvider), - Tag: git.GitTag, + // Unblock downstream work. To be addressed more generally later. + if git := src.GitSource; git != nil { + dst.GitSource = &schema.ResourceJobGitSource{ + Url: git.GitUrl, + Branch: git.GitBranch, + Commit: git.GitCommit, + Provider: string(git.GitProvider), + Tag: git.GitTag, + } } } tfroot.Resource.Job[k] = &dst + + // Configure permissions for this resource. + if rp := convPermissions(src.Permissions); rp != nil { + rp.JobId = fmt.Sprintf("${databricks_job.%s.id}", k) + tfroot.Resource.Permissions["job_"+k] = rp + } } for k, src := range config.Resources.Pipelines { var dst schema.ResourcePipeline conv(src, &dst) - for _, v := range src.Libraries { - var l schema.ResourcePipelineLibrary - conv(v, &l) - dst.Library = append(dst.Library, l) - } + if src.PipelineSpec != nil { + for _, v := range src.Libraries { + var l schema.ResourcePipelineLibrary + conv(v, &l) + dst.Library = append(dst.Library, l) + } - for _, v := range src.Clusters { - var l schema.ResourcePipelineCluster - conv(v, &l) - dst.Cluster = append(dst.Cluster, l) + for _, v := range src.Clusters { + var l schema.ResourcePipelineCluster + conv(v, &l) + dst.Cluster = append(dst.Cluster, l) + } } tfroot.Resource.Pipeline[k] = &dst + + // Configure permissions for this resource. + if rp := convPermissions(src.Permissions); rp != nil { + rp.PipelineId = fmt.Sprintf("${databricks_pipeline.%s.id}", k) + tfroot.Resource.Permissions["pipeline_"+k] = rp + } } for k, src := range config.Resources.Models { var dst schema.ResourceMlflowModel conv(src, &dst) tfroot.Resource.MlflowModel[k] = &dst + + // Configure permissions for this resource. + if rp := convPermissions(src.Permissions); rp != nil { + rp.RegisteredModelId = fmt.Sprintf("${databricks_mlflow_model.%s.registered_model_id}", k) + tfroot.Resource.Permissions["mlflow_model_"+k] = rp + } } for k, src := range config.Resources.Experiments { var dst schema.ResourceMlflowExperiment conv(src, &dst) tfroot.Resource.MlflowExperiment[k] = &dst + + // Configure permissions for this resource. + if rp := convPermissions(src.Permissions); rp != nil { + rp.ExperimentId = fmt.Sprintf("${databricks_mlflow_experiment.%s.id}", k) + tfroot.Resource.Permissions["mlflow_experiment_"+k] = rp + } } return tfroot diff --git a/bundle/deploy/terraform/convert_test.go b/bundle/deploy/terraform/convert_test.go index 60c91a2ca..2d833ffd6 100644 --- a/bundle/deploy/terraform/convert_test.go +++ b/bundle/deploy/terraform/convert_test.go @@ -47,6 +47,33 @@ func TestConvertJob(t *testing.T) { assert.Nil(t, out.Data) } +func TestConvertJobPermissions(t *testing.T) { + var src = resources.Job{ + Permissions: []resources.Permission{ + { + Level: "CAN_VIEW", + UserName: "jane@doe.com", + }, + }, + } + + var config = config.Root{ + Resources: config.Resources{ + Jobs: map[string]*resources.Job{ + "my_job": &src, + }, + }, + } + + out := BundleToTerraform(&config) + assert.NotEmpty(t, out.Resource.Permissions["job_my_job"].JobId) + assert.Len(t, out.Resource.Permissions["job_my_job"].AccessControl, 1) + + p := out.Resource.Permissions["job_my_job"].AccessControl[0] + assert.Equal(t, "jane@doe.com", p.UserName) + assert.Equal(t, "CAN_VIEW", p.PermissionLevel) +} + func TestConvertJobTaskLibraries(t *testing.T) { var src = resources.Job{ JobSettings: &jobs.JobSettings{ @@ -81,6 +108,33 @@ func TestConvertJobTaskLibraries(t *testing.T) { assert.Equal(t, "mlflow", out.Resource.Job["my_job"].Task[0].Library[0].Pypi.Package) } +func TestConvertPipelinePermissions(t *testing.T) { + var src = resources.Pipeline{ + Permissions: []resources.Permission{ + { + Level: "CAN_VIEW", + UserName: "jane@doe.com", + }, + }, + } + + var config = config.Root{ + Resources: config.Resources{ + Pipelines: map[string]*resources.Pipeline{ + "my_pipeline": &src, + }, + }, + } + + out := BundleToTerraform(&config) + assert.NotEmpty(t, out.Resource.Permissions["pipeline_my_pipeline"].PipelineId) + assert.Len(t, out.Resource.Permissions["pipeline_my_pipeline"].AccessControl, 1) + + p := out.Resource.Permissions["pipeline_my_pipeline"].AccessControl[0] + assert.Equal(t, "jane@doe.com", p.UserName) + assert.Equal(t, "CAN_VIEW", p.PermissionLevel) +} + func TestConvertModel(t *testing.T) { var src = resources.MlflowModel{ RegisteredModel: &mlflow.RegisteredModel{ @@ -118,6 +172,33 @@ func TestConvertModel(t *testing.T) { assert.Nil(t, out.Data) } +func TestConvertModelPermissions(t *testing.T) { + var src = resources.MlflowModel{ + Permissions: []resources.Permission{ + { + Level: "CAN_READ", + UserName: "jane@doe.com", + }, + }, + } + + var config = config.Root{ + Resources: config.Resources{ + Models: map[string]*resources.MlflowModel{ + "my_model": &src, + }, + }, + } + + out := BundleToTerraform(&config) + assert.NotEmpty(t, out.Resource.Permissions["mlflow_model_my_model"].RegisteredModelId) + assert.Len(t, out.Resource.Permissions["mlflow_model_my_model"].AccessControl, 1) + + p := out.Resource.Permissions["mlflow_model_my_model"].AccessControl[0] + assert.Equal(t, "jane@doe.com", p.UserName) + assert.Equal(t, "CAN_READ", p.PermissionLevel) +} + func TestConvertExperiment(t *testing.T) { var src = resources.MlflowExperiment{ Experiment: &mlflow.Experiment{ @@ -137,3 +218,31 @@ func TestConvertExperiment(t *testing.T) { assert.Equal(t, "name", out.Resource.MlflowExperiment["my_experiment"].Name) assert.Nil(t, out.Data) } + +func TestConvertExperimentPermissions(t *testing.T) { + var src = resources.MlflowExperiment{ + Permissions: []resources.Permission{ + { + Level: "CAN_READ", + UserName: "jane@doe.com", + }, + }, + } + + var config = config.Root{ + Resources: config.Resources{ + Experiments: map[string]*resources.MlflowExperiment{ + "my_experiment": &src, + }, + }, + } + + out := BundleToTerraform(&config) + assert.NotEmpty(t, out.Resource.Permissions["mlflow_experiment_my_experiment"].ExperimentId) + assert.Len(t, out.Resource.Permissions["mlflow_experiment_my_experiment"].AccessControl, 1) + + p := out.Resource.Permissions["mlflow_experiment_my_experiment"].AccessControl[0] + assert.Equal(t, "jane@doe.com", p.UserName) + assert.Equal(t, "CAN_READ", p.PermissionLevel) + +}