Add "bundle summary" command (#1123)

The plan is to use the new command in the Databricks VSCode extension to
render "modified" UI state in the bundle resource tree elements, plus
use resource IDs to generate links for the resources

### New revision
- Renamed `remote-state` to `summary`
- Added "modified statuses" to all resources. Currently we don't set
"updated" status - it's either nothing, or created/deleted
- Added tests for the `TerraformToBundle` command
This commit is contained in:
Ilia Babanov 2024-01-25 12:32:47 +01:00 committed by GitHub
parent 8988920a3e
commit 9c3e4fda7c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 604 additions and 72 deletions

View File

@ -10,6 +10,7 @@ import (
type Job struct { type Job struct {
ID string `json:"id,omitempty" bundle:"readonly"` ID string `json:"id,omitempty" bundle:"readonly"`
Permissions []Permission `json:"permissions,omitempty"` Permissions []Permission `json:"permissions,omitempty"`
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
paths.Paths paths.Paths

View File

@ -7,7 +7,9 @@ import (
) )
type MlflowExperiment struct { type MlflowExperiment struct {
ID string `json:"id,omitempty" bundle:"readonly"`
Permissions []Permission `json:"permissions,omitempty"` Permissions []Permission `json:"permissions,omitempty"`
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
paths.Paths paths.Paths

View File

@ -7,7 +7,9 @@ import (
) )
type MlflowModel struct { type MlflowModel struct {
ID string `json:"id,omitempty" bundle:"readonly"`
Permissions []Permission `json:"permissions,omitempty"` Permissions []Permission `json:"permissions,omitempty"`
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
paths.Paths paths.Paths

View File

@ -13,7 +13,7 @@ type ModelServingEndpoint struct {
// This represents the id (ie serving_endpoint_id) that can be used // This represents the id (ie serving_endpoint_id) that can be used
// as a reference in other resources. This value is returned by terraform. // as a reference in other resources. This value is returned by terraform.
ID string ID string `json:"id,omitempty" bundle:"readonly"`
// Path to config file where the resource is defined. All bundle resources // Path to config file where the resource is defined. All bundle resources
// include this for interpolation purposes. // include this for interpolation purposes.
@ -22,6 +22,8 @@ type ModelServingEndpoint struct {
// This is a resource agnostic implementation of permissions for ACLs. // This is a resource agnostic implementation of permissions for ACLs.
// Implementation could be different based on the resource type. // Implementation could be different based on the resource type.
Permissions []Permission `json:"permissions,omitempty"` Permissions []Permission `json:"permissions,omitempty"`
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
} }
func (s *ModelServingEndpoint) UnmarshalJSON(b []byte) error { func (s *ModelServingEndpoint) UnmarshalJSON(b []byte) error {

View File

@ -0,0 +1,15 @@
package resources
// ModifiedStatus is an enum of the possible statuses of a resource from the local perspective.
// CREATED - new resources that have been added to the local bundle configuration and don't yet exist remotely.
// DELETED - existing resources that have been removed from the local bundle but still exist remotely.
// UPDATED - existing resources that have been modified.
// An empty status means that the resource is unchanged.
// We use these statuses to build git-status-like UI of the resources in the Databricks VSCode extension.
type ModifiedStatus = string
const (
ModifiedStatusCreated ModifiedStatus = "created"
ModifiedStatusUpdated ModifiedStatus = "updated"
ModifiedStatusDeleted ModifiedStatus = "deleted"
)

View File

@ -12,6 +12,7 @@ import (
type Pipeline struct { type Pipeline struct {
ID string `json:"id,omitempty" bundle:"readonly"` ID string `json:"id,omitempty" bundle:"readonly"`
Permissions []Permission `json:"permissions,omitempty"` Permissions []Permission `json:"permissions,omitempty"`
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
paths.Paths paths.Paths

View File

@ -14,7 +14,7 @@ type RegisteredModel struct {
// This represents the id which is the full name of the model // This represents the id which is the full name of the model
// (catalog_name.schema_name.model_name) that can be used // (catalog_name.schema_name.model_name) that can be used
// as a reference in other resources. This value is returned by terraform. // as a reference in other resources. This value is returned by terraform.
ID string ID string `json:"id,omitempty" bundle:"readonly"`
// Path to config file where the resource is defined. All bundle resources // Path to config file where the resource is defined. All bundle resources
// include this for interpolation purposes. // include this for interpolation purposes.
@ -23,6 +23,8 @@ type RegisteredModel struct {
// This represents the input args for terraform, and will get converted // This represents the input args for terraform, and will get converted
// to a HCL representation for CRUD // to a HCL representation for CRUD
*catalog.CreateRegisteredModelRequest *catalog.CreateRegisteredModelRequest
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
} }
func (s *RegisteredModel) UnmarshalJSON(b []byte) error { func (s *RegisteredModel) UnmarshalJSON(b []byte) error {

View File

@ -3,6 +3,7 @@ package terraform
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect"
"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"
@ -15,6 +16,15 @@ func conv(from any, to any) {
json.Unmarshal(buf, &to) json.Unmarshal(buf, &to)
} }
func convRemoteToLocal(remote any, local any) resources.ModifiedStatus {
var modifiedStatus resources.ModifiedStatus
if reflect.ValueOf(local).Elem().IsNil() {
modifiedStatus = resources.ModifiedStatusDeleted
}
conv(remote, local)
return modifiedStatus
}
func convPermissions(acl []resources.Permission) *schema.ResourcePermissions { func convPermissions(acl []resources.Permission) *schema.ResourcePermissions {
if len(acl) == 0 { if len(acl) == 0 {
return nil return nil
@ -219,11 +229,7 @@ func BundleToTerraform(config *config.Root) *schema.Root {
} }
func TerraformToBundle(state *tfjson.State, config *config.Root) error { func TerraformToBundle(state *tfjson.State, config *config.Root) error {
// This is a no-op if the state is empty. if state.Values != nil && state.Values.RootModule != nil {
if state.Values == nil || state.Values.RootModule == nil {
return nil
}
for _, resource := range state.Values.RootModule.Resources { for _, resource := range state.Values.RootModule.Resources {
// Limit to resources. // Limit to resources.
if resource.Mode != tfjson.ManagedResourceMode { if resource.Mode != tfjson.ManagedResourceMode {
@ -234,38 +240,63 @@ func TerraformToBundle(state *tfjson.State, config *config.Root) error {
case "databricks_job": case "databricks_job":
var tmp schema.ResourceJob var tmp schema.ResourceJob
conv(resource.AttributeValues, &tmp) conv(resource.AttributeValues, &tmp)
if config.Resources.Jobs == nil {
config.Resources.Jobs = make(map[string]*resources.Job)
}
cur := config.Resources.Jobs[resource.Name] cur := config.Resources.Jobs[resource.Name]
conv(tmp, &cur) // TODO: make sure we can unmarshall tf state properly and don't swallow errors
modifiedStatus := convRemoteToLocal(tmp, &cur)
cur.ModifiedStatus = modifiedStatus
config.Resources.Jobs[resource.Name] = cur config.Resources.Jobs[resource.Name] = cur
case "databricks_pipeline": case "databricks_pipeline":
var tmp schema.ResourcePipeline var tmp schema.ResourcePipeline
conv(resource.AttributeValues, &tmp) conv(resource.AttributeValues, &tmp)
if config.Resources.Pipelines == nil {
config.Resources.Pipelines = make(map[string]*resources.Pipeline)
}
cur := config.Resources.Pipelines[resource.Name] cur := config.Resources.Pipelines[resource.Name]
conv(tmp, &cur) modifiedStatus := convRemoteToLocal(tmp, &cur)
cur.ModifiedStatus = modifiedStatus
config.Resources.Pipelines[resource.Name] = cur config.Resources.Pipelines[resource.Name] = cur
case "databricks_mlflow_model": case "databricks_mlflow_model":
var tmp schema.ResourceMlflowModel var tmp schema.ResourceMlflowModel
conv(resource.AttributeValues, &tmp) conv(resource.AttributeValues, &tmp)
if config.Resources.Models == nil {
config.Resources.Models = make(map[string]*resources.MlflowModel)
}
cur := config.Resources.Models[resource.Name] cur := config.Resources.Models[resource.Name]
conv(tmp, &cur) modifiedStatus := convRemoteToLocal(tmp, &cur)
cur.ModifiedStatus = modifiedStatus
config.Resources.Models[resource.Name] = cur config.Resources.Models[resource.Name] = cur
case "databricks_mlflow_experiment": case "databricks_mlflow_experiment":
var tmp schema.ResourceMlflowExperiment var tmp schema.ResourceMlflowExperiment
conv(resource.AttributeValues, &tmp) conv(resource.AttributeValues, &tmp)
if config.Resources.Experiments == nil {
config.Resources.Experiments = make(map[string]*resources.MlflowExperiment)
}
cur := config.Resources.Experiments[resource.Name] cur := config.Resources.Experiments[resource.Name]
conv(tmp, &cur) modifiedStatus := convRemoteToLocal(tmp, &cur)
cur.ModifiedStatus = modifiedStatus
config.Resources.Experiments[resource.Name] = cur config.Resources.Experiments[resource.Name] = cur
case "databricks_model_serving": case "databricks_model_serving":
var tmp schema.ResourceModelServing var tmp schema.ResourceModelServing
conv(resource.AttributeValues, &tmp) conv(resource.AttributeValues, &tmp)
if config.Resources.ModelServingEndpoints == nil {
config.Resources.ModelServingEndpoints = make(map[string]*resources.ModelServingEndpoint)
}
cur := config.Resources.ModelServingEndpoints[resource.Name] cur := config.Resources.ModelServingEndpoints[resource.Name]
conv(tmp, &cur) modifiedStatus := convRemoteToLocal(tmp, &cur)
cur.ModifiedStatus = modifiedStatus
config.Resources.ModelServingEndpoints[resource.Name] = cur config.Resources.ModelServingEndpoints[resource.Name] = cur
case "databricks_registered_model": case "databricks_registered_model":
var tmp schema.ResourceRegisteredModel var tmp schema.ResourceRegisteredModel
conv(resource.AttributeValues, &tmp) conv(resource.AttributeValues, &tmp)
if config.Resources.RegisteredModels == nil {
config.Resources.RegisteredModels = make(map[string]*resources.RegisteredModel)
}
cur := config.Resources.RegisteredModels[resource.Name] cur := config.Resources.RegisteredModels[resource.Name]
conv(tmp, &cur) modifiedStatus := convRemoteToLocal(tmp, &cur)
cur.ModifiedStatus = modifiedStatus
config.Resources.RegisteredModels[resource.Name] = cur config.Resources.RegisteredModels[resource.Name] = cur
case "databricks_permissions": case "databricks_permissions":
case "databricks_grants": case "databricks_grants":
@ -274,6 +305,38 @@ func TerraformToBundle(state *tfjson.State, config *config.Root) error {
return fmt.Errorf("missing mapping for %s", resource.Type) return fmt.Errorf("missing mapping for %s", resource.Type)
} }
} }
}
for _, src := range config.Resources.Jobs {
if src.ModifiedStatus == "" && src.ID == "" {
src.ModifiedStatus = resources.ModifiedStatusCreated
}
}
for _, src := range config.Resources.Pipelines {
if src.ModifiedStatus == "" && src.ID == "" {
src.ModifiedStatus = resources.ModifiedStatusCreated
}
}
for _, src := range config.Resources.Models {
if src.ModifiedStatus == "" && src.ID == "" {
src.ModifiedStatus = resources.ModifiedStatusCreated
}
}
for _, src := range config.Resources.Experiments {
if src.ModifiedStatus == "" && src.ID == "" {
src.ModifiedStatus = resources.ModifiedStatusCreated
}
}
for _, src := range config.Resources.ModelServingEndpoints {
if src.ModifiedStatus == "" && src.ID == "" {
src.ModifiedStatus = resources.ModifiedStatusCreated
}
}
for _, src := range config.Resources.RegisteredModels {
if src.ModifiedStatus == "" && src.ID == "" {
src.ModifiedStatus = resources.ModifiedStatusCreated
}
}
return nil return nil
} }

View File

@ -1,6 +1,7 @@
package terraform package terraform
import ( import (
"reflect"
"testing" "testing"
"github.com/databricks/cli/bundle/config" "github.com/databricks/cli/bundle/config"
@ -11,11 +12,12 @@ import (
"github.com/databricks/databricks-sdk-go/service/ml" "github.com/databricks/databricks-sdk-go/service/ml"
"github.com/databricks/databricks-sdk-go/service/pipelines" "github.com/databricks/databricks-sdk-go/service/pipelines"
"github.com/databricks/databricks-sdk-go/service/serving" "github.com/databricks/databricks-sdk-go/service/serving"
tfjson "github.com/hashicorp/terraform-json"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestConvertJob(t *testing.T) { func TestBundleToTerraformJob(t *testing.T) {
var src = resources.Job{ var src = resources.Job{
JobSettings: &jobs.JobSettings{ JobSettings: &jobs.JobSettings{
Name: "my job", Name: "my job",
@ -62,7 +64,7 @@ func TestConvertJob(t *testing.T) {
assert.Nil(t, out.Data) assert.Nil(t, out.Data)
} }
func TestConvertJobPermissions(t *testing.T) { func TestBundleToTerraformJobPermissions(t *testing.T) {
var src = resources.Job{ var src = resources.Job{
Permissions: []resources.Permission{ Permissions: []resources.Permission{
{ {
@ -89,7 +91,7 @@ func TestConvertJobPermissions(t *testing.T) {
assert.Equal(t, "CAN_VIEW", p.PermissionLevel) assert.Equal(t, "CAN_VIEW", p.PermissionLevel)
} }
func TestConvertJobTaskLibraries(t *testing.T) { func TestBundleToTerraformJobTaskLibraries(t *testing.T) {
var src = resources.Job{ var src = resources.Job{
JobSettings: &jobs.JobSettings{ JobSettings: &jobs.JobSettings{
Name: "my job", Name: "my job",
@ -123,7 +125,7 @@ func TestConvertJobTaskLibraries(t *testing.T) {
assert.Equal(t, "mlflow", out.Resource.Job["my_job"].Task[0].Library[0].Pypi.Package) assert.Equal(t, "mlflow", out.Resource.Job["my_job"].Task[0].Library[0].Pypi.Package)
} }
func TestConvertPipeline(t *testing.T) { func TestBundleToTerraformPipeline(t *testing.T) {
var src = resources.Pipeline{ var src = resources.Pipeline{
PipelineSpec: &pipelines.PipelineSpec{ PipelineSpec: &pipelines.PipelineSpec{
Name: "my pipeline", Name: "my pipeline",
@ -182,7 +184,7 @@ func TestConvertPipeline(t *testing.T) {
assert.Nil(t, out.Data) assert.Nil(t, out.Data)
} }
func TestConvertPipelinePermissions(t *testing.T) { func TestBundleToTerraformPipelinePermissions(t *testing.T) {
var src = resources.Pipeline{ var src = resources.Pipeline{
Permissions: []resources.Permission{ Permissions: []resources.Permission{
{ {
@ -209,7 +211,7 @@ func TestConvertPipelinePermissions(t *testing.T) {
assert.Equal(t, "CAN_VIEW", p.PermissionLevel) assert.Equal(t, "CAN_VIEW", p.PermissionLevel)
} }
func TestConvertModel(t *testing.T) { func TestBundleToTerraformModel(t *testing.T) {
var src = resources.MlflowModel{ var src = resources.MlflowModel{
Model: &ml.Model{ Model: &ml.Model{
Name: "name", Name: "name",
@ -246,7 +248,7 @@ func TestConvertModel(t *testing.T) {
assert.Nil(t, out.Data) assert.Nil(t, out.Data)
} }
func TestConvertModelPermissions(t *testing.T) { func TestBundleToTerraformModelPermissions(t *testing.T) {
var src = resources.MlflowModel{ var src = resources.MlflowModel{
Permissions: []resources.Permission{ Permissions: []resources.Permission{
{ {
@ -273,7 +275,7 @@ func TestConvertModelPermissions(t *testing.T) {
assert.Equal(t, "CAN_READ", p.PermissionLevel) assert.Equal(t, "CAN_READ", p.PermissionLevel)
} }
func TestConvertExperiment(t *testing.T) { func TestBundleToTerraformExperiment(t *testing.T) {
var src = resources.MlflowExperiment{ var src = resources.MlflowExperiment{
Experiment: &ml.Experiment{ Experiment: &ml.Experiment{
Name: "name", Name: "name",
@ -293,7 +295,7 @@ func TestConvertExperiment(t *testing.T) {
assert.Nil(t, out.Data) assert.Nil(t, out.Data)
} }
func TestConvertExperimentPermissions(t *testing.T) { func TestBundleToTerraformExperimentPermissions(t *testing.T) {
var src = resources.MlflowExperiment{ var src = resources.MlflowExperiment{
Permissions: []resources.Permission{ Permissions: []resources.Permission{
{ {
@ -321,7 +323,7 @@ func TestConvertExperimentPermissions(t *testing.T) {
} }
func TestConvertModelServing(t *testing.T) { func TestBundleToTerraformModelServing(t *testing.T) {
var src = resources.ModelServingEndpoint{ var src = resources.ModelServingEndpoint{
CreateServingEndpoint: &serving.CreateServingEndpoint{ CreateServingEndpoint: &serving.CreateServingEndpoint{
Name: "name", Name: "name",
@ -366,7 +368,7 @@ func TestConvertModelServing(t *testing.T) {
assert.Nil(t, out.Data) assert.Nil(t, out.Data)
} }
func TestConvertModelServingPermissions(t *testing.T) { func TestBundleToTerraformModelServingPermissions(t *testing.T) {
var src = resources.ModelServingEndpoint{ var src = resources.ModelServingEndpoint{
Permissions: []resources.Permission{ Permissions: []resources.Permission{
{ {
@ -394,7 +396,7 @@ func TestConvertModelServingPermissions(t *testing.T) {
} }
func TestConvertRegisteredModel(t *testing.T) { func TestBundleToTerraformRegisteredModel(t *testing.T) {
var src = resources.RegisteredModel{ var src = resources.RegisteredModel{
CreateRegisteredModelRequest: &catalog.CreateRegisteredModelRequest{ CreateRegisteredModelRequest: &catalog.CreateRegisteredModelRequest{
Name: "name", Name: "name",
@ -421,7 +423,7 @@ func TestConvertRegisteredModel(t *testing.T) {
assert.Nil(t, out.Data) assert.Nil(t, out.Data)
} }
func TestConvertRegisteredModelGrants(t *testing.T) { func TestBundleToTerraformRegisteredModelGrants(t *testing.T) {
var src = resources.RegisteredModel{ var src = resources.RegisteredModel{
Grants: []resources.Grant{ Grants: []resources.Grant{
{ {
@ -446,5 +448,370 @@ func TestConvertRegisteredModelGrants(t *testing.T) {
p := out.Resource.Grants["registered_model_my_registered_model"].Grant[0] p := out.Resource.Grants["registered_model_my_registered_model"].Grant[0]
assert.Equal(t, "jane@doe.com", p.Principal) assert.Equal(t, "jane@doe.com", p.Principal)
assert.Equal(t, "EXECUTE", p.Privileges[0]) assert.Equal(t, "EXECUTE", p.Privileges[0])
}
func TestTerraformToBundleEmptyLocalResources(t *testing.T) {
var config = config.Root{
Resources: config.Resources{},
}
var tfState = tfjson.State{
Values: &tfjson.StateValues{
RootModule: &tfjson.StateModule{
Resources: []*tfjson.StateResource{
{
Type: "databricks_job",
Mode: "managed",
Name: "test_job",
AttributeValues: map[string]interface{}{"id": "1"},
},
{
Type: "databricks_pipeline",
Mode: "managed",
Name: "test_pipeline",
AttributeValues: map[string]interface{}{"id": "1"},
},
{
Type: "databricks_mlflow_model",
Mode: "managed",
Name: "test_mlflow_model",
AttributeValues: map[string]interface{}{"id": "1"},
},
{
Type: "databricks_mlflow_experiment",
Mode: "managed",
Name: "test_mlflow_experiment",
AttributeValues: map[string]interface{}{"id": "1"},
},
{
Type: "databricks_model_serving",
Mode: "managed",
Name: "test_model_serving",
AttributeValues: map[string]interface{}{"id": "1"},
},
{
Type: "databricks_registered_model",
Mode: "managed",
Name: "test_registered_model",
AttributeValues: map[string]interface{}{"id": "1"},
},
},
},
},
}
err := TerraformToBundle(&tfState, &config)
assert.NoError(t, err)
assert.Equal(t, "1", config.Resources.Jobs["test_job"].ID)
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.Jobs["test_job"].ModifiedStatus)
assert.Equal(t, "1", config.Resources.Pipelines["test_pipeline"].ID)
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.Pipelines["test_pipeline"].ModifiedStatus)
assert.Equal(t, "1", config.Resources.Models["test_mlflow_model"].ID)
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.Models["test_mlflow_model"].ModifiedStatus)
assert.Equal(t, "1", config.Resources.Experiments["test_mlflow_experiment"].ID)
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.Experiments["test_mlflow_experiment"].ModifiedStatus)
assert.Equal(t, "1", config.Resources.ModelServingEndpoints["test_model_serving"].ID)
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.ModelServingEndpoints["test_model_serving"].ModifiedStatus)
assert.Equal(t, "1", config.Resources.RegisteredModels["test_registered_model"].ID)
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.RegisteredModels["test_registered_model"].ModifiedStatus)
AssertFullResourceCoverage(t, &config)
}
func TestTerraformToBundleEmptyRemoteResources(t *testing.T) {
var config = config.Root{
Resources: config.Resources{
Jobs: map[string]*resources.Job{
"test_job": {
JobSettings: &jobs.JobSettings{
Name: "test_job",
},
},
},
Pipelines: map[string]*resources.Pipeline{
"test_pipeline": {
PipelineSpec: &pipelines.PipelineSpec{
Name: "test_pipeline",
},
},
},
Models: map[string]*resources.MlflowModel{
"test_mlflow_model": {
Model: &ml.Model{
Name: "test_mlflow_model",
},
},
},
Experiments: map[string]*resources.MlflowExperiment{
"test_mlflow_experiment": {
Experiment: &ml.Experiment{
Name: "test_mlflow_experiment",
},
},
},
ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{
"test_model_serving": {
CreateServingEndpoint: &serving.CreateServingEndpoint{
Name: "test_model_serving",
},
},
},
RegisteredModels: map[string]*resources.RegisteredModel{
"test_registered_model": {
CreateRegisteredModelRequest: &catalog.CreateRegisteredModelRequest{
Name: "test_registered_model",
},
},
},
},
}
var tfState = tfjson.State{
Values: nil,
}
err := TerraformToBundle(&tfState, &config)
assert.NoError(t, err)
assert.Equal(t, "", config.Resources.Jobs["test_job"].ID)
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.Jobs["test_job"].ModifiedStatus)
assert.Equal(t, "", config.Resources.Pipelines["test_pipeline"].ID)
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.Pipelines["test_pipeline"].ModifiedStatus)
assert.Equal(t, "", config.Resources.Models["test_mlflow_model"].ID)
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.Models["test_mlflow_model"].ModifiedStatus)
assert.Equal(t, "", config.Resources.Experiments["test_mlflow_experiment"].ID)
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.Experiments["test_mlflow_experiment"].ModifiedStatus)
assert.Equal(t, "", config.Resources.ModelServingEndpoints["test_model_serving"].ID)
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.ModelServingEndpoints["test_model_serving"].ModifiedStatus)
assert.Equal(t, "", config.Resources.RegisteredModels["test_registered_model"].ID)
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.RegisteredModels["test_registered_model"].ModifiedStatus)
AssertFullResourceCoverage(t, &config)
}
func TestTerraformToBundleModifiedResources(t *testing.T) {
var config = config.Root{
Resources: config.Resources{
Jobs: map[string]*resources.Job{
"test_job": {
JobSettings: &jobs.JobSettings{
Name: "test_job",
},
},
"test_job_new": {
JobSettings: &jobs.JobSettings{
Name: "test_job_new",
},
},
},
Pipelines: map[string]*resources.Pipeline{
"test_pipeline": {
PipelineSpec: &pipelines.PipelineSpec{
Name: "test_pipeline",
},
},
"test_pipeline_new": {
PipelineSpec: &pipelines.PipelineSpec{
Name: "test_pipeline_new",
},
},
},
Models: map[string]*resources.MlflowModel{
"test_mlflow_model": {
Model: &ml.Model{
Name: "test_mlflow_model",
},
},
"test_mlflow_model_new": {
Model: &ml.Model{
Name: "test_mlflow_model_new",
},
},
},
Experiments: map[string]*resources.MlflowExperiment{
"test_mlflow_experiment": {
Experiment: &ml.Experiment{
Name: "test_mlflow_experiment",
},
},
"test_mlflow_experiment_new": {
Experiment: &ml.Experiment{
Name: "test_mlflow_experiment_new",
},
},
},
ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{
"test_model_serving": {
CreateServingEndpoint: &serving.CreateServingEndpoint{
Name: "test_model_serving",
},
},
"test_model_serving_new": {
CreateServingEndpoint: &serving.CreateServingEndpoint{
Name: "test_model_serving_new",
},
},
},
RegisteredModels: map[string]*resources.RegisteredModel{
"test_registered_model": {
CreateRegisteredModelRequest: &catalog.CreateRegisteredModelRequest{
Name: "test_registered_model",
},
},
"test_registered_model_new": {
CreateRegisteredModelRequest: &catalog.CreateRegisteredModelRequest{
Name: "test_registered_model_new",
},
},
},
},
}
var tfState = tfjson.State{
Values: &tfjson.StateValues{
RootModule: &tfjson.StateModule{
Resources: []*tfjson.StateResource{
{
Type: "databricks_job",
Mode: "managed",
Name: "test_job",
AttributeValues: map[string]interface{}{"id": "1"},
},
{
Type: "databricks_job",
Mode: "managed",
Name: "test_job_old",
AttributeValues: map[string]interface{}{"id": "2"},
},
{
Type: "databricks_pipeline",
Mode: "managed",
Name: "test_pipeline",
AttributeValues: map[string]interface{}{"id": "1"},
},
{
Type: "databricks_pipeline",
Mode: "managed",
Name: "test_pipeline_old",
AttributeValues: map[string]interface{}{"id": "2"},
},
{
Type: "databricks_mlflow_model",
Mode: "managed",
Name: "test_mlflow_model",
AttributeValues: map[string]interface{}{"id": "1"},
},
{
Type: "databricks_mlflow_model",
Mode: "managed",
Name: "test_mlflow_model_old",
AttributeValues: map[string]interface{}{"id": "2"},
},
{
Type: "databricks_mlflow_experiment",
Mode: "managed",
Name: "test_mlflow_experiment",
AttributeValues: map[string]interface{}{"id": "1"},
},
{
Type: "databricks_mlflow_experiment",
Mode: "managed",
Name: "test_mlflow_experiment_old",
AttributeValues: map[string]interface{}{"id": "2"},
},
{
Type: "databricks_model_serving",
Mode: "managed",
Name: "test_model_serving",
AttributeValues: map[string]interface{}{"id": "1"},
},
{
Type: "databricks_model_serving",
Mode: "managed",
Name: "test_model_serving_old",
AttributeValues: map[string]interface{}{"id": "2"},
},
{
Type: "databricks_registered_model",
Mode: "managed",
Name: "test_registered_model",
AttributeValues: map[string]interface{}{"id": "1"},
},
{
Type: "databricks_registered_model",
Mode: "managed",
Name: "test_registered_model_old",
AttributeValues: map[string]interface{}{"id": "2"},
},
},
},
},
}
err := TerraformToBundle(&tfState, &config)
assert.NoError(t, err)
assert.Equal(t, "1", config.Resources.Jobs["test_job"].ID)
assert.Equal(t, "", config.Resources.Jobs["test_job"].ModifiedStatus)
assert.Equal(t, "2", config.Resources.Jobs["test_job_old"].ID)
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.Jobs["test_job_old"].ModifiedStatus)
assert.Equal(t, "", config.Resources.Jobs["test_job_new"].ID)
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.Jobs["test_job_new"].ModifiedStatus)
assert.Equal(t, "1", config.Resources.Pipelines["test_pipeline"].ID)
assert.Equal(t, "", config.Resources.Pipelines["test_pipeline"].ModifiedStatus)
assert.Equal(t, "2", config.Resources.Pipelines["test_pipeline_old"].ID)
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.Pipelines["test_pipeline_old"].ModifiedStatus)
assert.Equal(t, "", config.Resources.Pipelines["test_pipeline_new"].ID)
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.Pipelines["test_pipeline_new"].ModifiedStatus)
assert.Equal(t, "1", config.Resources.Models["test_mlflow_model"].ID)
assert.Equal(t, "", config.Resources.Models["test_mlflow_model"].ModifiedStatus)
assert.Equal(t, "2", config.Resources.Models["test_mlflow_model_old"].ID)
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.Models["test_mlflow_model_old"].ModifiedStatus)
assert.Equal(t, "", config.Resources.Models["test_mlflow_model_new"].ID)
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.Models["test_mlflow_model_new"].ModifiedStatus)
assert.Equal(t, "1", config.Resources.RegisteredModels["test_registered_model"].ID)
assert.Equal(t, "", config.Resources.RegisteredModels["test_registered_model"].ModifiedStatus)
assert.Equal(t, "2", config.Resources.RegisteredModels["test_registered_model_old"].ID)
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.RegisteredModels["test_registered_model_old"].ModifiedStatus)
assert.Equal(t, "", config.Resources.RegisteredModels["test_registered_model_new"].ID)
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.RegisteredModels["test_registered_model_new"].ModifiedStatus)
assert.Equal(t, "1", config.Resources.Experiments["test_mlflow_experiment"].ID)
assert.Equal(t, "", config.Resources.Experiments["test_mlflow_experiment"].ModifiedStatus)
assert.Equal(t, "2", config.Resources.Experiments["test_mlflow_experiment_old"].ID)
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.Experiments["test_mlflow_experiment_old"].ModifiedStatus)
assert.Equal(t, "", config.Resources.Experiments["test_mlflow_experiment_new"].ID)
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.Experiments["test_mlflow_experiment_new"].ModifiedStatus)
assert.Equal(t, "1", config.Resources.ModelServingEndpoints["test_model_serving"].ID)
assert.Equal(t, "", config.Resources.ModelServingEndpoints["test_model_serving"].ModifiedStatus)
assert.Equal(t, "2", config.Resources.ModelServingEndpoints["test_model_serving_old"].ID)
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.ModelServingEndpoints["test_model_serving_old"].ModifiedStatus)
assert.Equal(t, "", config.Resources.ModelServingEndpoints["test_model_serving_new"].ID)
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.ModelServingEndpoints["test_model_serving_new"].ModifiedStatus)
AssertFullResourceCoverage(t, &config)
}
func AssertFullResourceCoverage(t *testing.T, config *config.Root) {
resources := reflect.ValueOf(config.Resources)
for i := 0; i < resources.NumField(); i++ {
field := resources.Field(i)
if field.Kind() == reflect.Map {
assert.True(
t,
!field.IsNil() && field.Len() > 0,
"TerraformToBundle should support '%s' (please add it to convert.go and extend the test suite)",
resources.Type().Field(i).Name,
)
}
}
} }

View File

@ -22,6 +22,7 @@ func New() *cobra.Command {
cmd.AddCommand(newTestCommand()) cmd.AddCommand(newTestCommand())
cmd.AddCommand(newValidateCommand()) cmd.AddCommand(newValidateCommand())
cmd.AddCommand(newInitCommand()) cmd.AddCommand(newInitCommand())
cmd.AddCommand(newSummaryCommand())
cmd.AddCommand(newGenerateCommand()) cmd.AddCommand(newGenerateCommand())
return cmd return cmd
} }

76
cmd/bundle/summary.go Normal file
View File

@ -0,0 +1,76 @@
package bundle
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/deploy/terraform"
"github.com/databricks/cli/bundle/phases"
"github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/flags"
"github.com/spf13/cobra"
)
func newSummaryCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "summary",
Short: "Describe the bundle resources and their deployment states",
PreRunE: ConfigureBundleWithVariables,
// This command is currently intended for the Databricks VSCode extension only
Hidden: true,
}
var forcePull bool
cmd.Flags().BoolVar(&forcePull, "force-pull", false, "Skip local cache and load the state from the remote workspace")
cmd.RunE = func(cmd *cobra.Command, args []string) error {
b := bundle.Get(cmd.Context())
err := bundle.Apply(cmd.Context(), b, phases.Initialize())
if err != nil {
return err
}
cacheDir, err := terraform.Dir(cmd.Context(), b)
if err != nil {
return err
}
_, err = os.Stat(filepath.Join(cacheDir, terraform.TerraformStateFileName))
noCache := errors.Is(err, os.ErrNotExist)
if forcePull || noCache {
err = bundle.Apply(cmd.Context(), b, terraform.StatePull())
if err != nil {
return err
}
}
err = bundle.Apply(cmd.Context(), b, terraform.Load())
if err != nil {
return err
}
switch root.OutputType(cmd) {
case flags.OutputText:
return fmt.Errorf("%w, only json output is supported", errors.ErrUnsupported)
case flags.OutputJSON:
buf, err := json.MarshalIndent(b.Config, "", " ")
if err != nil {
return err
}
cmd.OutOrStdout().Write(buf)
default:
return fmt.Errorf("unknown output type %s", root.OutputType(cmd))
}
return nil
}
return cmd
}