mirror of https://github.com/databricks/cli.git
Compare commits
6 Commits
d2272fade5
...
6cfab91c59
Author | SHA1 | Date |
---|---|---|
|
6cfab91c59 | |
|
e53e7489a2 | |
|
eefda8c198 | |
|
c9770503f8 | |
|
3b1fb6d2df | |
|
0c9c90208f |
|
@ -2,9 +2,7 @@ package terraform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/databricks/cli/bundle/config"
|
"github.com/databricks/cli/bundle/config"
|
||||||
"github.com/databricks/cli/bundle/config/resources"
|
"github.com/databricks/cli/bundle/config/resources"
|
||||||
|
@ -14,244 +12,6 @@ import (
|
||||||
tfjson "github.com/hashicorp/terraform-json"
|
tfjson "github.com/hashicorp/terraform-json"
|
||||||
)
|
)
|
||||||
|
|
||||||
func conv(from any, to any) {
|
|
||||||
buf, _ := json.Marshal(from)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func convGrants(acl []resources.Grant) *schema.ResourceGrants {
|
|
||||||
if len(acl) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
resource := schema.ResourceGrants{}
|
|
||||||
for _, ac := range acl {
|
|
||||||
resource.Grant = append(resource.Grant, schema.ResourceGrantsGrant{
|
|
||||||
Privileges: ac.Privileges,
|
|
||||||
Principal: ac.Principal,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return &resource
|
|
||||||
}
|
|
||||||
|
|
||||||
// BundleToTerraform converts resources in a bundle configuration
|
|
||||||
// to the equivalent Terraform JSON representation.
|
|
||||||
//
|
|
||||||
// Note: This function is an older implementation of the conversion logic. It is
|
|
||||||
// no longer used in any code paths. It is kept around to be used in tests.
|
|
||||||
// New resources do not need to modify this function and can instead can define
|
|
||||||
// the conversion login in the tfdyn package.
|
|
||||||
func BundleToTerraform(config *config.Root) *schema.Root {
|
|
||||||
tfroot := schema.NewRoot()
|
|
||||||
tfroot.Provider = schema.NewProviders()
|
|
||||||
tfroot.Resource = schema.NewResources()
|
|
||||||
noResources := true
|
|
||||||
|
|
||||||
for k, src := range config.Resources.Jobs {
|
|
||||||
noResources = false
|
|
||||||
var dst schema.ResourceJob
|
|
||||||
conv(src, &dst)
|
|
||||||
|
|
||||||
if src.JobSettings != nil {
|
|
||||||
sort.Slice(src.JobSettings.Tasks, func(i, j int) bool {
|
|
||||||
return src.JobSettings.Tasks[i].TaskKey < src.JobSettings.Tasks[j].TaskKey
|
|
||||||
})
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert for_each_task libraries
|
|
||||||
if v.ForEachTask != nil {
|
|
||||||
for _, v_ := range v.ForEachTask.Task.Libraries {
|
|
||||||
var l schema.ResourceJobTaskForEachTaskTaskLibrary
|
|
||||||
conv(v_, &l)
|
|
||||||
t.ForEachTask.Task.Library = append(t.ForEachTask.Task.Library, l)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
dst.Task = append(dst.Task, 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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range src.Parameters {
|
|
||||||
var t schema.ResourceJobParameter
|
|
||||||
conv(v, &t)
|
|
||||||
dst.Parameter = append(dst.Parameter, t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
noResources = false
|
|
||||||
var dst schema.ResourcePipeline
|
|
||||||
conv(src, &dst)
|
|
||||||
|
|
||||||
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.Notifications {
|
|
||||||
var l schema.ResourcePipelineNotification
|
|
||||||
conv(v, &l)
|
|
||||||
dst.Notification = append(dst.Notification, 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 {
|
|
||||||
noResources = false
|
|
||||||
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 {
|
|
||||||
noResources = false
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, src := range config.Resources.ModelServingEndpoints {
|
|
||||||
noResources = false
|
|
||||||
var dst schema.ResourceModelServing
|
|
||||||
conv(src, &dst)
|
|
||||||
tfroot.Resource.ModelServing[k] = &dst
|
|
||||||
|
|
||||||
// Configure permissions for this resource.
|
|
||||||
if rp := convPermissions(src.Permissions); rp != nil {
|
|
||||||
rp.ServingEndpointId = fmt.Sprintf("${databricks_model_serving.%s.serving_endpoint_id}", k)
|
|
||||||
tfroot.Resource.Permissions["model_serving_"+k] = rp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, src := range config.Resources.RegisteredModels {
|
|
||||||
noResources = false
|
|
||||||
var dst schema.ResourceRegisteredModel
|
|
||||||
conv(src, &dst)
|
|
||||||
tfroot.Resource.RegisteredModel[k] = &dst
|
|
||||||
|
|
||||||
// Configure permissions for this resource.
|
|
||||||
if rp := convGrants(src.Grants); rp != nil {
|
|
||||||
rp.Function = fmt.Sprintf("${databricks_registered_model.%s.id}", k)
|
|
||||||
tfroot.Resource.Grants["registered_model_"+k] = rp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, src := range config.Resources.QualityMonitors {
|
|
||||||
noResources = false
|
|
||||||
var dst schema.ResourceQualityMonitor
|
|
||||||
conv(src, &dst)
|
|
||||||
tfroot.Resource.QualityMonitor[k] = &dst
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, src := range config.Resources.Clusters {
|
|
||||||
noResources = false
|
|
||||||
var dst schema.ResourceCluster
|
|
||||||
conv(src, &dst)
|
|
||||||
tfroot.Resource.Cluster[k] = &dst
|
|
||||||
}
|
|
||||||
|
|
||||||
// We explicitly set "resource" to nil to omit it from a JSON encoding.
|
|
||||||
// This is required because the terraform CLI requires >= 1 resources defined
|
|
||||||
// if the "resource" property is used in a .tf.json file.
|
|
||||||
if noResources {
|
|
||||||
tfroot.Resource = nil
|
|
||||||
}
|
|
||||||
return tfroot
|
|
||||||
}
|
|
||||||
|
|
||||||
// BundleToTerraformWithDynValue converts resources in a bundle configuration
|
// BundleToTerraformWithDynValue converts resources in a bundle configuration
|
||||||
// to the equivalent Terraform JSON representation.
|
// to the equivalent Terraform JSON representation.
|
||||||
func BundleToTerraformWithDynValue(ctx context.Context, root dyn.Value) (*schema.Root, error) {
|
func BundleToTerraformWithDynValue(ctx context.Context, root dyn.Value) (*schema.Root, error) {
|
||||||
|
|
|
@ -2,7 +2,6 @@ package terraform
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -21,6 +20,27 @@ import (
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func produceTerraformConfiguration(t *testing.T, config config.Root) *schema.Root {
|
||||||
|
vin, err := convert.FromTyped(config, dyn.NilValue)
|
||||||
|
require.NoError(t, err)
|
||||||
|
out, err := BundleToTerraformWithDynValue(context.Background(), vin)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertToResourceStruct[T any](t *testing.T, resource *T, data any) {
|
||||||
|
require.NotNil(t, resource)
|
||||||
|
require.NotNil(t, data)
|
||||||
|
|
||||||
|
// Convert data to a dyn.Value.
|
||||||
|
vin, err := convert.FromTyped(data, dyn.NilValue)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Convert the dyn.Value to a struct.
|
||||||
|
err = convert.ToTyped(resource, vin)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestBundleToTerraformJob(t *testing.T) {
|
func TestBundleToTerraformJob(t *testing.T) {
|
||||||
var src = resources.Job{
|
var src = resources.Job{
|
||||||
JobSettings: &jobs.JobSettings{
|
JobSettings: &jobs.JobSettings{
|
||||||
|
@ -58,8 +78,9 @@ func TestBundleToTerraformJob(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := BundleToTerraform(&config)
|
var resource schema.ResourceJob
|
||||||
resource := out.Resource.Job["my_job"].(*schema.ResourceJob)
|
out := produceTerraformConfiguration(t, config)
|
||||||
|
convertToResourceStruct(t, &resource, out.Resource.Job["my_job"])
|
||||||
|
|
||||||
assert.Equal(t, "my job", resource.Name)
|
assert.Equal(t, "my job", resource.Name)
|
||||||
assert.Len(t, resource.JobCluster, 1)
|
assert.Len(t, resource.JobCluster, 1)
|
||||||
|
@ -68,8 +89,6 @@ func TestBundleToTerraformJob(t *testing.T) {
|
||||||
assert.Equal(t, "param1", resource.Parameter[0].Name)
|
assert.Equal(t, "param1", resource.Parameter[0].Name)
|
||||||
assert.Equal(t, "param2", resource.Parameter[1].Name)
|
assert.Equal(t, "param2", resource.Parameter[1].Name)
|
||||||
assert.Nil(t, out.Data)
|
assert.Nil(t, out.Data)
|
||||||
|
|
||||||
bundleToTerraformEquivalenceTest(t, &config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundleToTerraformJobPermissions(t *testing.T) {
|
func TestBundleToTerraformJobPermissions(t *testing.T) {
|
||||||
|
@ -90,15 +109,14 @@ func TestBundleToTerraformJobPermissions(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := BundleToTerraform(&config)
|
var resource schema.ResourcePermissions
|
||||||
resource := out.Resource.Permissions["job_my_job"].(*schema.ResourcePermissions)
|
out := produceTerraformConfiguration(t, config)
|
||||||
|
convertToResourceStruct(t, &resource, out.Resource.Permissions["job_my_job"])
|
||||||
|
|
||||||
assert.NotEmpty(t, resource.JobId)
|
assert.NotEmpty(t, resource.JobId)
|
||||||
assert.Len(t, resource.AccessControl, 1)
|
assert.Len(t, resource.AccessControl, 1)
|
||||||
assert.Equal(t, "jane@doe.com", resource.AccessControl[0].UserName)
|
assert.Equal(t, "jane@doe.com", resource.AccessControl[0].UserName)
|
||||||
assert.Equal(t, "CAN_VIEW", resource.AccessControl[0].PermissionLevel)
|
assert.Equal(t, "CAN_VIEW", resource.AccessControl[0].PermissionLevel)
|
||||||
|
|
||||||
bundleToTerraformEquivalenceTest(t, &config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundleToTerraformJobTaskLibraries(t *testing.T) {
|
func TestBundleToTerraformJobTaskLibraries(t *testing.T) {
|
||||||
|
@ -128,15 +146,14 @@ func TestBundleToTerraformJobTaskLibraries(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := BundleToTerraform(&config)
|
var resource schema.ResourceJob
|
||||||
resource := out.Resource.Job["my_job"].(*schema.ResourceJob)
|
out := produceTerraformConfiguration(t, config)
|
||||||
|
convertToResourceStruct(t, &resource, out.Resource.Job["my_job"])
|
||||||
|
|
||||||
assert.Equal(t, "my job", resource.Name)
|
assert.Equal(t, "my job", resource.Name)
|
||||||
require.Len(t, resource.Task, 1)
|
require.Len(t, resource.Task, 1)
|
||||||
require.Len(t, resource.Task[0].Library, 1)
|
require.Len(t, resource.Task[0].Library, 1)
|
||||||
assert.Equal(t, "mlflow", resource.Task[0].Library[0].Pypi.Package)
|
assert.Equal(t, "mlflow", resource.Task[0].Library[0].Pypi.Package)
|
||||||
|
|
||||||
bundleToTerraformEquivalenceTest(t, &config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundleToTerraformForEachTaskLibraries(t *testing.T) {
|
func TestBundleToTerraformForEachTaskLibraries(t *testing.T) {
|
||||||
|
@ -172,15 +189,14 @@ func TestBundleToTerraformForEachTaskLibraries(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := BundleToTerraform(&config)
|
var resource schema.ResourceJob
|
||||||
resource := out.Resource.Job["my_job"].(*schema.ResourceJob)
|
out := produceTerraformConfiguration(t, config)
|
||||||
|
convertToResourceStruct(t, &resource, out.Resource.Job["my_job"])
|
||||||
|
|
||||||
assert.Equal(t, "my job", resource.Name)
|
assert.Equal(t, "my job", resource.Name)
|
||||||
require.Len(t, resource.Task, 1)
|
require.Len(t, resource.Task, 1)
|
||||||
require.Len(t, resource.Task[0].ForEachTask.Task.Library, 1)
|
require.Len(t, resource.Task[0].ForEachTask.Task.Library, 1)
|
||||||
assert.Equal(t, "mlflow", resource.Task[0].ForEachTask.Task.Library[0].Pypi.Package)
|
assert.Equal(t, "mlflow", resource.Task[0].ForEachTask.Task.Library[0].Pypi.Package)
|
||||||
|
|
||||||
bundleToTerraformEquivalenceTest(t, &config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundleToTerraformPipeline(t *testing.T) {
|
func TestBundleToTerraformPipeline(t *testing.T) {
|
||||||
|
@ -230,8 +246,9 @@ func TestBundleToTerraformPipeline(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := BundleToTerraform(&config)
|
var resource schema.ResourcePipeline
|
||||||
resource := out.Resource.Pipeline["my_pipeline"].(*schema.ResourcePipeline)
|
out := produceTerraformConfiguration(t, config)
|
||||||
|
convertToResourceStruct(t, &resource, out.Resource.Pipeline["my_pipeline"])
|
||||||
|
|
||||||
assert.Equal(t, "my pipeline", resource.Name)
|
assert.Equal(t, "my pipeline", resource.Name)
|
||||||
assert.Len(t, resource.Library, 2)
|
assert.Len(t, resource.Library, 2)
|
||||||
|
@ -241,8 +258,6 @@ func TestBundleToTerraformPipeline(t *testing.T) {
|
||||||
assert.Equal(t, resource.Notification[1].Alerts, []string{"on-update-failure", "on-flow-failure"})
|
assert.Equal(t, resource.Notification[1].Alerts, []string{"on-update-failure", "on-flow-failure"})
|
||||||
assert.Equal(t, resource.Notification[1].EmailRecipients, []string{"jane@doe.com", "john@doe.com"})
|
assert.Equal(t, resource.Notification[1].EmailRecipients, []string{"jane@doe.com", "john@doe.com"})
|
||||||
assert.Nil(t, out.Data)
|
assert.Nil(t, out.Data)
|
||||||
|
|
||||||
bundleToTerraformEquivalenceTest(t, &config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundleToTerraformPipelinePermissions(t *testing.T) {
|
func TestBundleToTerraformPipelinePermissions(t *testing.T) {
|
||||||
|
@ -263,15 +278,14 @@ func TestBundleToTerraformPipelinePermissions(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := BundleToTerraform(&config)
|
var resource schema.ResourcePermissions
|
||||||
resource := out.Resource.Permissions["pipeline_my_pipeline"].(*schema.ResourcePermissions)
|
out := produceTerraformConfiguration(t, config)
|
||||||
|
convertToResourceStruct(t, &resource, out.Resource.Permissions["pipeline_my_pipeline"])
|
||||||
|
|
||||||
assert.NotEmpty(t, resource.PipelineId)
|
assert.NotEmpty(t, resource.PipelineId)
|
||||||
assert.Len(t, resource.AccessControl, 1)
|
assert.Len(t, resource.AccessControl, 1)
|
||||||
assert.Equal(t, "jane@doe.com", resource.AccessControl[0].UserName)
|
assert.Equal(t, "jane@doe.com", resource.AccessControl[0].UserName)
|
||||||
assert.Equal(t, "CAN_VIEW", resource.AccessControl[0].PermissionLevel)
|
assert.Equal(t, "CAN_VIEW", resource.AccessControl[0].PermissionLevel)
|
||||||
|
|
||||||
bundleToTerraformEquivalenceTest(t, &config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundleToTerraformModel(t *testing.T) {
|
func TestBundleToTerraformModel(t *testing.T) {
|
||||||
|
@ -300,8 +314,9 @@ func TestBundleToTerraformModel(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := BundleToTerraform(&config)
|
var resource schema.ResourceMlflowModel
|
||||||
resource := out.Resource.MlflowModel["my_model"].(*schema.ResourceMlflowModel)
|
out := produceTerraformConfiguration(t, config)
|
||||||
|
convertToResourceStruct(t, &resource, out.Resource.MlflowModel["my_model"])
|
||||||
|
|
||||||
assert.Equal(t, "name", resource.Name)
|
assert.Equal(t, "name", resource.Name)
|
||||||
assert.Equal(t, "description", resource.Description)
|
assert.Equal(t, "description", resource.Description)
|
||||||
|
@ -311,8 +326,6 @@ func TestBundleToTerraformModel(t *testing.T) {
|
||||||
assert.Equal(t, "k2", resource.Tags[1].Key)
|
assert.Equal(t, "k2", resource.Tags[1].Key)
|
||||||
assert.Equal(t, "v2", resource.Tags[1].Value)
|
assert.Equal(t, "v2", resource.Tags[1].Value)
|
||||||
assert.Nil(t, out.Data)
|
assert.Nil(t, out.Data)
|
||||||
|
|
||||||
bundleToTerraformEquivalenceTest(t, &config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundleToTerraformModelPermissions(t *testing.T) {
|
func TestBundleToTerraformModelPermissions(t *testing.T) {
|
||||||
|
@ -336,15 +349,14 @@ func TestBundleToTerraformModelPermissions(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := BundleToTerraform(&config)
|
var resource schema.ResourcePermissions
|
||||||
resource := out.Resource.Permissions["mlflow_model_my_model"].(*schema.ResourcePermissions)
|
out := produceTerraformConfiguration(t, config)
|
||||||
|
convertToResourceStruct(t, &resource, out.Resource.Permissions["mlflow_model_my_model"])
|
||||||
|
|
||||||
assert.NotEmpty(t, resource.RegisteredModelId)
|
assert.NotEmpty(t, resource.RegisteredModelId)
|
||||||
assert.Len(t, resource.AccessControl, 1)
|
assert.Len(t, resource.AccessControl, 1)
|
||||||
assert.Equal(t, "jane@doe.com", resource.AccessControl[0].UserName)
|
assert.Equal(t, "jane@doe.com", resource.AccessControl[0].UserName)
|
||||||
assert.Equal(t, "CAN_READ", resource.AccessControl[0].PermissionLevel)
|
assert.Equal(t, "CAN_READ", resource.AccessControl[0].PermissionLevel)
|
||||||
|
|
||||||
bundleToTerraformEquivalenceTest(t, &config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundleToTerraformExperiment(t *testing.T) {
|
func TestBundleToTerraformExperiment(t *testing.T) {
|
||||||
|
@ -362,13 +374,12 @@ func TestBundleToTerraformExperiment(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := BundleToTerraform(&config)
|
var resource schema.ResourceMlflowExperiment
|
||||||
resource := out.Resource.MlflowExperiment["my_experiment"].(*schema.ResourceMlflowExperiment)
|
out := produceTerraformConfiguration(t, config)
|
||||||
|
convertToResourceStruct(t, &resource, out.Resource.MlflowExperiment["my_experiment"])
|
||||||
|
|
||||||
assert.Equal(t, "name", resource.Name)
|
assert.Equal(t, "name", resource.Name)
|
||||||
assert.Nil(t, out.Data)
|
assert.Nil(t, out.Data)
|
||||||
|
|
||||||
bundleToTerraformEquivalenceTest(t, &config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundleToTerraformExperimentPermissions(t *testing.T) {
|
func TestBundleToTerraformExperimentPermissions(t *testing.T) {
|
||||||
|
@ -392,15 +403,14 @@ func TestBundleToTerraformExperimentPermissions(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := BundleToTerraform(&config)
|
var resource schema.ResourcePermissions
|
||||||
resource := out.Resource.Permissions["mlflow_experiment_my_experiment"].(*schema.ResourcePermissions)
|
out := produceTerraformConfiguration(t, config)
|
||||||
|
convertToResourceStruct(t, &resource, out.Resource.Permissions["mlflow_experiment_my_experiment"])
|
||||||
|
|
||||||
assert.NotEmpty(t, resource.ExperimentId)
|
assert.NotEmpty(t, resource.ExperimentId)
|
||||||
assert.Len(t, resource.AccessControl, 1)
|
assert.Len(t, resource.AccessControl, 1)
|
||||||
assert.Equal(t, "jane@doe.com", resource.AccessControl[0].UserName)
|
assert.Equal(t, "jane@doe.com", resource.AccessControl[0].UserName)
|
||||||
assert.Equal(t, "CAN_READ", resource.AccessControl[0].PermissionLevel)
|
assert.Equal(t, "CAN_READ", resource.AccessControl[0].PermissionLevel)
|
||||||
|
|
||||||
bundleToTerraformEquivalenceTest(t, &config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundleToTerraformModelServing(t *testing.T) {
|
func TestBundleToTerraformModelServing(t *testing.T) {
|
||||||
|
@ -436,8 +446,9 @@ func TestBundleToTerraformModelServing(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := BundleToTerraform(&config)
|
var resource schema.ResourceModelServing
|
||||||
resource := out.Resource.ModelServing["my_model_serving_endpoint"].(*schema.ResourceModelServing)
|
out := produceTerraformConfiguration(t, config)
|
||||||
|
convertToResourceStruct(t, &resource, out.Resource.ModelServing["my_model_serving_endpoint"])
|
||||||
|
|
||||||
assert.Equal(t, "name", resource.Name)
|
assert.Equal(t, "name", resource.Name)
|
||||||
assert.Equal(t, "model_name", resource.Config.ServedModels[0].ModelName)
|
assert.Equal(t, "model_name", resource.Config.ServedModels[0].ModelName)
|
||||||
|
@ -447,8 +458,6 @@ func TestBundleToTerraformModelServing(t *testing.T) {
|
||||||
assert.Equal(t, "model_name-1", resource.Config.TrafficConfig.Routes[0].ServedModelName)
|
assert.Equal(t, "model_name-1", resource.Config.TrafficConfig.Routes[0].ServedModelName)
|
||||||
assert.Equal(t, 100, resource.Config.TrafficConfig.Routes[0].TrafficPercentage)
|
assert.Equal(t, 100, resource.Config.TrafficConfig.Routes[0].TrafficPercentage)
|
||||||
assert.Nil(t, out.Data)
|
assert.Nil(t, out.Data)
|
||||||
|
|
||||||
bundleToTerraformEquivalenceTest(t, &config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundleToTerraformModelServingPermissions(t *testing.T) {
|
func TestBundleToTerraformModelServingPermissions(t *testing.T) {
|
||||||
|
@ -490,15 +499,14 @@ func TestBundleToTerraformModelServingPermissions(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := BundleToTerraform(&config)
|
var resource schema.ResourcePermissions
|
||||||
resource := out.Resource.Permissions["model_serving_my_model_serving_endpoint"].(*schema.ResourcePermissions)
|
out := produceTerraformConfiguration(t, config)
|
||||||
|
convertToResourceStruct(t, &resource, out.Resource.Permissions["model_serving_my_model_serving_endpoint"])
|
||||||
|
|
||||||
assert.NotEmpty(t, resource.ServingEndpointId)
|
assert.NotEmpty(t, resource.ServingEndpointId)
|
||||||
assert.Len(t, resource.AccessControl, 1)
|
assert.Len(t, resource.AccessControl, 1)
|
||||||
assert.Equal(t, "jane@doe.com", resource.AccessControl[0].UserName)
|
assert.Equal(t, "jane@doe.com", resource.AccessControl[0].UserName)
|
||||||
assert.Equal(t, "CAN_VIEW", resource.AccessControl[0].PermissionLevel)
|
assert.Equal(t, "CAN_VIEW", resource.AccessControl[0].PermissionLevel)
|
||||||
|
|
||||||
bundleToTerraformEquivalenceTest(t, &config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundleToTerraformRegisteredModel(t *testing.T) {
|
func TestBundleToTerraformRegisteredModel(t *testing.T) {
|
||||||
|
@ -519,16 +527,15 @@ func TestBundleToTerraformRegisteredModel(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := BundleToTerraform(&config)
|
var resource schema.ResourceRegisteredModel
|
||||||
resource := out.Resource.RegisteredModel["my_registered_model"].(*schema.ResourceRegisteredModel)
|
out := produceTerraformConfiguration(t, config)
|
||||||
|
convertToResourceStruct(t, &resource, out.Resource.RegisteredModel["my_registered_model"])
|
||||||
|
|
||||||
assert.Equal(t, "name", resource.Name)
|
assert.Equal(t, "name", resource.Name)
|
||||||
assert.Equal(t, "catalog", resource.CatalogName)
|
assert.Equal(t, "catalog", resource.CatalogName)
|
||||||
assert.Equal(t, "schema", resource.SchemaName)
|
assert.Equal(t, "schema", resource.SchemaName)
|
||||||
assert.Equal(t, "comment", resource.Comment)
|
assert.Equal(t, "comment", resource.Comment)
|
||||||
assert.Nil(t, out.Data)
|
assert.Nil(t, out.Data)
|
||||||
|
|
||||||
bundleToTerraformEquivalenceTest(t, &config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundleToTerraformRegisteredModelGrants(t *testing.T) {
|
func TestBundleToTerraformRegisteredModelGrants(t *testing.T) {
|
||||||
|
@ -554,15 +561,14 @@ func TestBundleToTerraformRegisteredModelGrants(t *testing.T) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
out := BundleToTerraform(&config)
|
var resource schema.ResourceGrants
|
||||||
resource := out.Resource.Grants["registered_model_my_registered_model"].(*schema.ResourceGrants)
|
out := produceTerraformConfiguration(t, config)
|
||||||
|
convertToResourceStruct(t, &resource, out.Resource.Grants["registered_model_my_registered_model"])
|
||||||
|
|
||||||
assert.NotEmpty(t, resource.Function)
|
assert.NotEmpty(t, resource.Function)
|
||||||
assert.Len(t, resource.Grant, 1)
|
assert.Len(t, resource.Grant, 1)
|
||||||
assert.Equal(t, "jane@doe.com", resource.Grant[0].Principal)
|
assert.Equal(t, "jane@doe.com", resource.Grant[0].Principal)
|
||||||
assert.Equal(t, "EXECUTE", resource.Grant[0].Privileges[0])
|
assert.Equal(t, "EXECUTE", resource.Grant[0].Privileges[0])
|
||||||
|
|
||||||
bundleToTerraformEquivalenceTest(t, &config)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBundleToTerraformDeletedResources(t *testing.T) {
|
func TestBundleToTerraformDeletedResources(t *testing.T) {
|
||||||
|
@ -1154,25 +1160,3 @@ func AssertFullResourceCoverage(t *testing.T, config *config.Root) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertEqualTerraformRoot(t *testing.T, a, b *schema.Root) {
|
|
||||||
ba, err := json.Marshal(a)
|
|
||||||
require.NoError(t, err)
|
|
||||||
bb, err := json.Marshal(b)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.JSONEq(t, string(ba), string(bb))
|
|
||||||
}
|
|
||||||
|
|
||||||
func bundleToTerraformEquivalenceTest(t *testing.T, config *config.Root) {
|
|
||||||
t.Run("dyn equivalence", func(t *testing.T) {
|
|
||||||
tf1 := BundleToTerraform(config)
|
|
||||||
|
|
||||||
vin, err := convert.FromTyped(config, dyn.NilValue)
|
|
||||||
require.NoError(t, err)
|
|
||||||
tf2, err := BundleToTerraformWithDynValue(context.Background(), vin)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
// Compare roots
|
|
||||||
assertEqualTerraformRoot(t, tf1, tf2)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
package permissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/bundle"
|
||||||
|
"github.com/databricks/cli/libs/diag"
|
||||||
|
)
|
||||||
|
|
||||||
|
type validateSharedRootPermissions struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateSharedRootPermissions() bundle.Mutator {
|
||||||
|
return &validateSharedRootPermissions{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*validateSharedRootPermissions) Name() string {
|
||||||
|
return "ValidateSharedRootPermissions"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*validateSharedRootPermissions) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||||
|
if isWorkspaceSharedRoot(b.Config.Workspace.RootPath) {
|
||||||
|
return isUsersGroupPermissionSet(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWorkspaceSharedRoot(path string) bool {
|
||||||
|
return strings.HasPrefix(path, "/Workspace/Shared/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// isUsersGroupPermissionSet checks that top-level permissions set for bundle contain group_name: users with CAN_MANAGE permission.
|
||||||
|
func isUsersGroupPermissionSet(b *bundle.Bundle) diag.Diagnostics {
|
||||||
|
var diags diag.Diagnostics
|
||||||
|
|
||||||
|
allUsers := false
|
||||||
|
for _, p := range b.Config.Permissions {
|
||||||
|
if p.GroupName == "users" && p.Level == CAN_MANAGE {
|
||||||
|
allUsers = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !allUsers {
|
||||||
|
diags = diags.Append(diag.Diagnostic{
|
||||||
|
Severity: diag.Warning,
|
||||||
|
Summary: fmt.Sprintf("the bundle root path %s is writable by all workspace users", b.Config.Workspace.RootPath),
|
||||||
|
Detail: "The bundle is configured to use /Workspace/Shared, which will give read/write access to all users. If this is intentional, add CAN_MANAGE for 'group_name: users' permission to your bundle configuration. If the deployment should be restricted, move it to a restricted folder such as /Workspace/Users/<username or principal name>.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return diags
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package permissions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/bundle"
|
||||||
|
"github.com/databricks/cli/bundle/config"
|
||||||
|
"github.com/databricks/cli/bundle/config/resources"
|
||||||
|
"github.com/databricks/cli/libs/diag"
|
||||||
|
"github.com/databricks/databricks-sdk-go/experimental/mocks"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/jobs"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateSharedRootPermissionsForShared(t *testing.T) {
|
||||||
|
b := &bundle.Bundle{
|
||||||
|
Config: config.Root{
|
||||||
|
Workspace: config.Workspace{
|
||||||
|
RootPath: "/Workspace/Shared/foo/bar",
|
||||||
|
},
|
||||||
|
Permissions: []resources.Permission{
|
||||||
|
{Level: CAN_MANAGE, GroupName: "users"},
|
||||||
|
},
|
||||||
|
Resources: config.Resources{
|
||||||
|
Jobs: map[string]*resources.Job{
|
||||||
|
"job_1": {JobSettings: &jobs.JobSettings{Name: "job_1"}},
|
||||||
|
"job_2": {JobSettings: &jobs.JobSettings{Name: "job_2"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := mocks.NewMockWorkspaceClient(t)
|
||||||
|
b.SetWorkpaceClient(m.WorkspaceClient)
|
||||||
|
|
||||||
|
diags := bundle.Apply(context.Background(), b, bundle.Seq(ValidateSharedRootPermissions()))
|
||||||
|
require.Empty(t, diags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSharedRootPermissionsForSharedError(t *testing.T) {
|
||||||
|
b := &bundle.Bundle{
|
||||||
|
Config: config.Root{
|
||||||
|
Workspace: config.Workspace{
|
||||||
|
RootPath: "/Workspace/Shared/foo/bar",
|
||||||
|
},
|
||||||
|
Permissions: []resources.Permission{
|
||||||
|
{Level: CAN_MANAGE, UserName: "foo@bar.com"},
|
||||||
|
},
|
||||||
|
Resources: config.Resources{
|
||||||
|
Jobs: map[string]*resources.Job{
|
||||||
|
"job_1": {JobSettings: &jobs.JobSettings{Name: "job_1"}},
|
||||||
|
"job_2": {JobSettings: &jobs.JobSettings{Name: "job_2"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
m := mocks.NewMockWorkspaceClient(t)
|
||||||
|
b.SetWorkpaceClient(m.WorkspaceClient)
|
||||||
|
|
||||||
|
diags := bundle.Apply(context.Background(), b, bundle.Seq(ValidateSharedRootPermissions()))
|
||||||
|
require.Len(t, diags, 1)
|
||||||
|
require.Equal(t, "the bundle root path /Workspace/Shared/foo/bar is writable by all workspace users", diags[0].Summary)
|
||||||
|
require.Equal(t, diag.Warning, diags[0].Severity)
|
||||||
|
}
|
|
@ -16,6 +16,10 @@ func ApplyWorkspaceRootPermissions() bundle.Mutator {
|
||||||
return &workspaceRootPermissions{}
|
return &workspaceRootPermissions{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*workspaceRootPermissions) Name() string {
|
||||||
|
return "ApplyWorkspaceRootPermissions"
|
||||||
|
}
|
||||||
|
|
||||||
// Apply implements bundle.Mutator.
|
// Apply implements bundle.Mutator.
|
||||||
func (*workspaceRootPermissions) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
func (*workspaceRootPermissions) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
|
||||||
err := giveAccessForWorkspaceRoot(ctx, b)
|
err := giveAccessForWorkspaceRoot(ctx, b)
|
||||||
|
@ -26,10 +30,6 @@ func (*workspaceRootPermissions) Apply(ctx context.Context, b *bundle.Bundle) di
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*workspaceRootPermissions) Name() string {
|
|
||||||
return "ApplyWorkspaceRootPermissions"
|
|
||||||
}
|
|
||||||
|
|
||||||
func giveAccessForWorkspaceRoot(ctx context.Context, b *bundle.Bundle) error {
|
func giveAccessForWorkspaceRoot(ctx context.Context, b *bundle.Bundle) error {
|
||||||
permissions := make([]workspace.WorkspaceObjectAccessControlRequest, 0)
|
permissions := make([]workspace.WorkspaceObjectAccessControlRequest, 0)
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,6 @@ func TestApplyWorkspaceRootPermissions(t *testing.T) {
|
||||||
WorkspaceObjectType: "directories",
|
WorkspaceObjectType: "directories",
|
||||||
}).Return(nil, nil)
|
}).Return(nil, nil)
|
||||||
|
|
||||||
diags := bundle.Apply(context.Background(), b, ApplyWorkspaceRootPermissions())
|
diags := bundle.Apply(context.Background(), b, bundle.Seq(ValidateSharedRootPermissions(), ApplyWorkspaceRootPermissions()))
|
||||||
require.NoError(t, diags.Error())
|
require.Empty(t, diags)
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,8 +76,11 @@ func Initialize() bundle.Mutator {
|
||||||
|
|
||||||
mutator.TranslatePaths(),
|
mutator.TranslatePaths(),
|
||||||
trampoline.WrapperWarning(),
|
trampoline.WrapperWarning(),
|
||||||
|
|
||||||
|
permissions.ValidateSharedRootPermissions(),
|
||||||
permissions.ApplyBundlePermissions(),
|
permissions.ApplyBundlePermissions(),
|
||||||
permissions.FilterCurrentUser(),
|
permissions.FilterCurrentUser(),
|
||||||
|
|
||||||
metadata.AnnotateJobs(),
|
metadata.AnnotateJobs(),
|
||||||
metadata.AnnotatePipelines(),
|
metadata.AnnotatePipelines(),
|
||||||
terraform.Initialize(),
|
terraform.Initialize(),
|
||||||
|
|
|
@ -2,11 +2,14 @@ package acc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/databricks/databricks-sdk-go"
|
"github.com/databricks/databricks-sdk-go"
|
||||||
|
"github.com/databricks/databricks-sdk-go/apierr"
|
||||||
"github.com/databricks/databricks-sdk-go/service/compute"
|
"github.com/databricks/databricks-sdk-go/service/compute"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/workspace"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -94,3 +97,30 @@ func (t *WorkspaceT) RunPython(code string) (string, error) {
|
||||||
require.True(t, ok, "unexpected type %T", results.Data)
|
require.True(t, ok, "unexpected type %T", results.Data)
|
||||||
return output, nil
|
return output, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *WorkspaceT) TemporaryWorkspaceDir(name ...string) string {
|
||||||
|
ctx := context.Background()
|
||||||
|
me, err := t.W.CurrentUser.Me(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
basePath := fmt.Sprintf("/Users/%s/%s", me.UserName, RandomName(name...))
|
||||||
|
|
||||||
|
t.Logf("Creating %s", basePath)
|
||||||
|
err = t.W.Workspace.MkdirsByPath(ctx, basePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Remove test directory on test completion.
|
||||||
|
t.Cleanup(func() {
|
||||||
|
t.Logf("Removing %s", basePath)
|
||||||
|
err := t.W.Workspace.Delete(ctx, workspace.Delete{
|
||||||
|
Path: basePath,
|
||||||
|
Recursive: true,
|
||||||
|
})
|
||||||
|
if err == nil || apierr.IsMissing(err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Logf("Unable to remove temporary workspace directory %s: %#v", basePath, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
return basePath
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/internal/acc"
|
||||||
|
"github.com/databricks/cli/libs/dyn"
|
||||||
|
"github.com/databricks/cli/libs/dyn/convert"
|
||||||
|
"github.com/databricks/cli/libs/dyn/merge"
|
||||||
|
"github.com/databricks/databricks-sdk-go/apierr"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/dashboards"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/workspace"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify that importing a dashboard through the Workspace API retains the identity of the underying resource,
|
||||||
|
// as well as properties exclusively accessible through the dashboards API.
|
||||||
|
func TestAccDashboardAssumptions_WorkspaceImport(t *testing.T) {
|
||||||
|
ctx, wt := acc.WorkspaceTest(t)
|
||||||
|
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
dashboardName := "New Dashboard"
|
||||||
|
dashboardPayload := []byte(`{"pages":[{"name":"2506f97a","displayName":"New Page"}]}`)
|
||||||
|
warehouseId := acc.GetEnvOrSkipTest(t, "TEST_DEFAULT_WAREHOUSE_ID")
|
||||||
|
|
||||||
|
dir := wt.TemporaryWorkspaceDir("dashboard-assumptions-")
|
||||||
|
|
||||||
|
dashboard, err := wt.W.Lakeview.Create(ctx, dashboards.CreateDashboardRequest{
|
||||||
|
DisplayName: dashboardName,
|
||||||
|
ParentPath: dir,
|
||||||
|
SerializedDashboard: string(dashboardPayload),
|
||||||
|
WarehouseId: warehouseId,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Logf("Dashboard ID (per Lakeview API): %s", dashboard.DashboardId)
|
||||||
|
|
||||||
|
// Overwrite the dashboard via the workspace API.
|
||||||
|
{
|
||||||
|
err := wt.W.Workspace.Import(ctx, workspace.Import{
|
||||||
|
Format: workspace.ImportFormatAuto,
|
||||||
|
Path: dashboard.Path,
|
||||||
|
Content: base64.StdEncoding.EncodeToString(dashboardPayload),
|
||||||
|
Overwrite: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-check consistency with the workspace object.
|
||||||
|
{
|
||||||
|
obj, err := wt.W.Workspace.GetStatusByPath(ctx, dashboard.Path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Confirm that the resource ID included in the response is equal to the dashboard ID.
|
||||||
|
require.Equal(t, dashboard.DashboardId, obj.ResourceId)
|
||||||
|
t.Logf("Dashboard ID (per workspace object status): %s", obj.ResourceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to overwrite the dashboard via the Lakeview API (and expect failure).
|
||||||
|
{
|
||||||
|
_, err := wt.W.Lakeview.Create(ctx, dashboards.CreateDashboardRequest{
|
||||||
|
DisplayName: dashboardName,
|
||||||
|
ParentPath: dir,
|
||||||
|
SerializedDashboard: string(dashboardPayload),
|
||||||
|
})
|
||||||
|
require.ErrorIs(t, err, apierr.ErrResourceAlreadyExists)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the dashboard object and confirm that only select fields were updated by the import.
|
||||||
|
{
|
||||||
|
previousDashboard := dashboard
|
||||||
|
currentDashboard, err := wt.W.Lakeview.Get(ctx, dashboards.GetDashboardRequest{
|
||||||
|
DashboardId: dashboard.DashboardId,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Convert the dashboard object to a [dyn.Value] to make comparison easier.
|
||||||
|
previous, err := convert.FromTyped(previousDashboard, dyn.NilValue)
|
||||||
|
require.NoError(t, err)
|
||||||
|
current, err := convert.FromTyped(currentDashboard, dyn.NilValue)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Collect updated paths.
|
||||||
|
var updatedFieldPaths []string
|
||||||
|
_, err = merge.Override(previous, current, merge.OverrideVisitor{
|
||||||
|
VisitDelete: func(basePath dyn.Path, left dyn.Value) error {
|
||||||
|
assert.Fail(t, "unexpected delete operation")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
VisitInsert: func(basePath dyn.Path, right dyn.Value) (dyn.Value, error) {
|
||||||
|
assert.Fail(t, "unexpected insert operation")
|
||||||
|
return right, nil
|
||||||
|
},
|
||||||
|
VisitUpdate: func(basePath dyn.Path, left dyn.Value, right dyn.Value) (dyn.Value, error) {
|
||||||
|
updatedFieldPaths = append(updatedFieldPaths, basePath.String())
|
||||||
|
return right, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Confirm that only the expected fields have been updated.
|
||||||
|
assert.ElementsMatch(t, []string{
|
||||||
|
"etag",
|
||||||
|
"update_time",
|
||||||
|
}, updatedFieldPaths)
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,17 +23,20 @@ type Repository struct {
|
||||||
// directory where we process .gitignore files.
|
// directory where we process .gitignore files.
|
||||||
real bool
|
real bool
|
||||||
|
|
||||||
// checkoutDir is the path to the root of the repository checkout.
|
// rootDir is the path to the root of the repository checkout.
|
||||||
// This can be either the main repository checkout or a worktree.
|
// This can be either the main repository checkout or a worktree checkout.
|
||||||
checkoutDir vfs.Path
|
// For more information about worktrees, see: https://git-scm.com/docs/git-worktree#_description.
|
||||||
|
rootDir vfs.Path
|
||||||
|
|
||||||
// gitDir is the equivalent of $GIT_DIR and points to the
|
// gitDir is the equivalent of $GIT_DIR and points to the
|
||||||
// `.git` directory of a repository or a worktree.
|
// `.git` directory of a repository or a worktree directory.
|
||||||
|
// See https://git-scm.com/docs/git-worktree#_details for more information.
|
||||||
gitDir vfs.Path
|
gitDir vfs.Path
|
||||||
|
|
||||||
// gitCommonDir is the equivalent of $GIT_COMMON_DIR and points to the
|
// gitCommonDir is the equivalent of $GIT_COMMON_DIR and points to the
|
||||||
// `.git` directory of the main working tree (common between worktrees).
|
// `.git` directory of the main working tree (common between worktrees).
|
||||||
// This is equivalent to [gitDir] if this is the main working tree.
|
// This is equivalent to [gitDir] if this is the main working tree.
|
||||||
|
// See https://git-scm.com/docs/git-worktree#_details for more information.
|
||||||
gitCommonDir vfs.Path
|
gitCommonDir vfs.Path
|
||||||
|
|
||||||
// ignore contains a list of ignore patterns indexed by the
|
// ignore contains a list of ignore patterns indexed by the
|
||||||
|
@ -54,7 +57,7 @@ type Repository struct {
|
||||||
|
|
||||||
// Root returns the absolute path to the repository root.
|
// Root returns the absolute path to the repository root.
|
||||||
func (r *Repository) Root() string {
|
func (r *Repository) Root() string {
|
||||||
return r.checkoutDir.Native()
|
return r.rootDir.Native()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) CurrentBranch() (string, error) {
|
func (r *Repository) CurrentBranch() (string, error) {
|
||||||
|
@ -151,7 +154,7 @@ func (r *Repository) getIgnoreRules(prefix string) []ignoreRules {
|
||||||
return fs
|
return fs
|
||||||
}
|
}
|
||||||
|
|
||||||
r.ignore[prefix] = append(r.ignore[prefix], newIgnoreFile(r.checkoutDir, path.Join(prefix, gitIgnoreFileName)))
|
r.ignore[prefix] = append(r.ignore[prefix], newIgnoreFile(r.rootDir, path.Join(prefix, gitIgnoreFileName)))
|
||||||
return r.ignore[prefix]
|
return r.ignore[prefix]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,7 +210,7 @@ func (r *Repository) Ignore(relPath string) (bool, error) {
|
||||||
|
|
||||||
func NewRepository(path vfs.Path) (*Repository, error) {
|
func NewRepository(path vfs.Path) (*Repository, error) {
|
||||||
real := true
|
real := true
|
||||||
checkoutDir, err := vfs.FindLeafInTree(path, GitDirectoryName)
|
rootDir, err := vfs.FindLeafInTree(path, GitDirectoryName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, fs.ErrNotExist) {
|
if !errors.Is(err, fs.ErrNotExist) {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -215,19 +218,19 @@ func NewRepository(path vfs.Path) (*Repository, error) {
|
||||||
// Cannot find `.git` directory.
|
// Cannot find `.git` directory.
|
||||||
// Treat the specified path as a potential repository root checkout.
|
// Treat the specified path as a potential repository root checkout.
|
||||||
real = false
|
real = false
|
||||||
checkoutDir = path
|
rootDir = path
|
||||||
}
|
}
|
||||||
|
|
||||||
// Derive $GIT_DIR and $GIT_COMMON_DIR paths if this is a real repository.
|
// Derive $GIT_DIR and $GIT_COMMON_DIR paths if this is a real repository.
|
||||||
// If it isn't a real repository, they'll point to the (non-existent) `.git` directory.
|
// If it isn't a real repository, they'll point to the (non-existent) `.git` directory.
|
||||||
gitDir, gitCommonDir, err := resolveGitDirs(checkoutDir)
|
gitDir, gitCommonDir, err := resolveGitDirs(rootDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
repo := &Repository{
|
repo := &Repository{
|
||||||
real: real,
|
real: real,
|
||||||
checkoutDir: checkoutDir,
|
rootDir: rootDir,
|
||||||
gitDir: gitDir,
|
gitDir: gitDir,
|
||||||
gitCommonDir: gitCommonDir,
|
gitCommonDir: gitCommonDir,
|
||||||
ignore: make(map[string][]ignoreRules),
|
ignore: make(map[string][]ignoreRules),
|
||||||
|
@ -263,10 +266,10 @@ func NewRepository(path vfs.Path) (*Repository, error) {
|
||||||
newStringIgnoreRules([]string{
|
newStringIgnoreRules([]string{
|
||||||
".git",
|
".git",
|
||||||
}),
|
}),
|
||||||
// Load repository-wide excludes file.
|
// Load repository-wide exclude file.
|
||||||
newIgnoreFile(repo.gitCommonDir, "info/excludes"),
|
newIgnoreFile(repo.gitCommonDir, "info/exclude"),
|
||||||
// Load root gitignore file.
|
// Load root gitignore file.
|
||||||
newIgnoreFile(repo.checkoutDir, ".gitignore"),
|
newIgnoreFile(repo.rootDir, ".gitignore"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return repo, nil
|
return repo, nil
|
||||||
|
|
|
@ -80,7 +80,7 @@ func NewView(root vfs.Path) (*View, error) {
|
||||||
|
|
||||||
// Target path must be relative to the repository root path.
|
// Target path must be relative to the repository root path.
|
||||||
target := root.Native()
|
target := root.Native()
|
||||||
prefix := repo.checkoutDir.Native()
|
prefix := repo.rootDir.Native()
|
||||||
if !strings.HasPrefix(target, prefix) {
|
if !strings.HasPrefix(target, prefix) {
|
||||||
return nil, fmt.Errorf("path %q is not within repository root %q", root.Native(), prefix)
|
return nil, fmt.Errorf("path %q is not within repository root %q", root.Native(), prefix)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue