mirror of https://github.com/databricks/cli.git
Add support for Lakehouse monitoring in bundles (#1307)
## Changes This change adds support for Lakehouse monitoring in bundles. The associated resource type name is "quality monitor". ## Testing Unit tests. --------- Co-authored-by: Pieter Noordhuis <pcnoordhuis@gmail.com> Co-authored-by: Pieter Noordhuis <pieter.noordhuis@databricks.com> Co-authored-by: Arpit Jasapara <87999496+arpitjasa-db@users.noreply.github.com>
This commit is contained in:
parent
364a609ea7
commit
a33d0c8bf9
|
@ -97,6 +97,9 @@ func mockBundle(mode config.Mode) *bundle.Bundle {
|
||||||
RegisteredModels: map[string]*resources.RegisteredModel{
|
RegisteredModels: map[string]*resources.RegisteredModel{
|
||||||
"registeredmodel1": {CreateRegisteredModelRequest: &catalog.CreateRegisteredModelRequest{Name: "registeredmodel1"}},
|
"registeredmodel1": {CreateRegisteredModelRequest: &catalog.CreateRegisteredModelRequest{Name: "registeredmodel1"}},
|
||||||
},
|
},
|
||||||
|
QualityMonitors: map[string]*resources.QualityMonitor{
|
||||||
|
"qualityMonitor1": {CreateMonitor: &catalog.CreateMonitor{TableName: "qualityMonitor1"}},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Use AWS implementation for testing.
|
// Use AWS implementation for testing.
|
||||||
|
@ -145,6 +148,9 @@ func TestProcessTargetModeDevelopment(t *testing.T) {
|
||||||
|
|
||||||
// Registered model 1
|
// Registered model 1
|
||||||
assert.Equal(t, "dev_lennart_registeredmodel1", b.Config.Resources.RegisteredModels["registeredmodel1"].Name)
|
assert.Equal(t, "dev_lennart_registeredmodel1", b.Config.Resources.RegisteredModels["registeredmodel1"].Name)
|
||||||
|
|
||||||
|
// Quality Monitor 1
|
||||||
|
assert.Equal(t, "qualityMonitor1", b.Config.Resources.QualityMonitors["qualityMonitor1"].TableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProcessTargetModeDevelopmentTagNormalizationForAws(t *testing.T) {
|
func TestProcessTargetModeDevelopmentTagNormalizationForAws(t *testing.T) {
|
||||||
|
@ -200,6 +206,7 @@ func TestProcessTargetModeDefault(t *testing.T) {
|
||||||
assert.False(t, b.Config.Resources.Pipelines["pipeline1"].PipelineSpec.Development)
|
assert.False(t, b.Config.Resources.Pipelines["pipeline1"].PipelineSpec.Development)
|
||||||
assert.Equal(t, "servingendpoint1", b.Config.Resources.ModelServingEndpoints["servingendpoint1"].Name)
|
assert.Equal(t, "servingendpoint1", b.Config.Resources.ModelServingEndpoints["servingendpoint1"].Name)
|
||||||
assert.Equal(t, "registeredmodel1", b.Config.Resources.RegisteredModels["registeredmodel1"].Name)
|
assert.Equal(t, "registeredmodel1", b.Config.Resources.RegisteredModels["registeredmodel1"].Name)
|
||||||
|
assert.Equal(t, "qualityMonitor1", b.Config.Resources.QualityMonitors["qualityMonitor1"].TableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProcessTargetModeProduction(t *testing.T) {
|
func TestProcessTargetModeProduction(t *testing.T) {
|
||||||
|
@ -240,6 +247,7 @@ func TestProcessTargetModeProduction(t *testing.T) {
|
||||||
assert.False(t, b.Config.Resources.Pipelines["pipeline1"].PipelineSpec.Development)
|
assert.False(t, b.Config.Resources.Pipelines["pipeline1"].PipelineSpec.Development)
|
||||||
assert.Equal(t, "servingendpoint1", b.Config.Resources.ModelServingEndpoints["servingendpoint1"].Name)
|
assert.Equal(t, "servingendpoint1", b.Config.Resources.ModelServingEndpoints["servingendpoint1"].Name)
|
||||||
assert.Equal(t, "registeredmodel1", b.Config.Resources.RegisteredModels["registeredmodel1"].Name)
|
assert.Equal(t, "registeredmodel1", b.Config.Resources.RegisteredModels["registeredmodel1"].Name)
|
||||||
|
assert.Equal(t, "qualityMonitor1", b.Config.Resources.QualityMonitors["qualityMonitor1"].TableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProcessTargetModeProductionOkForPrincipal(t *testing.T) {
|
func TestProcessTargetModeProductionOkForPrincipal(t *testing.T) {
|
||||||
|
|
|
@ -100,6 +100,16 @@ func validateRunAs(b *bundle.Bundle) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Monitors do not support run_as in the API.
|
||||||
|
if len(b.Config.Resources.QualityMonitors) > 0 {
|
||||||
|
return errUnsupportedResourceTypeForRunAs{
|
||||||
|
resourceType: "quality_monitors",
|
||||||
|
resourceLocation: b.Config.GetLocation("resources.quality_monitors"),
|
||||||
|
currentUser: b.Config.Workspace.CurrentUser.UserName,
|
||||||
|
runAsUser: identity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ func allResourceTypes(t *testing.T) []string {
|
||||||
"model_serving_endpoints",
|
"model_serving_endpoints",
|
||||||
"models",
|
"models",
|
||||||
"pipelines",
|
"pipelines",
|
||||||
|
"quality_monitors",
|
||||||
"registered_models",
|
"registered_models",
|
||||||
},
|
},
|
||||||
resourceTypes,
|
resourceTypes,
|
||||||
|
|
|
@ -17,6 +17,7 @@ type Resources struct {
|
||||||
Experiments map[string]*resources.MlflowExperiment `json:"experiments,omitempty"`
|
Experiments map[string]*resources.MlflowExperiment `json:"experiments,omitempty"`
|
||||||
ModelServingEndpoints map[string]*resources.ModelServingEndpoint `json:"model_serving_endpoints,omitempty"`
|
ModelServingEndpoints map[string]*resources.ModelServingEndpoint `json:"model_serving_endpoints,omitempty"`
|
||||||
RegisteredModels map[string]*resources.RegisteredModel `json:"registered_models,omitempty"`
|
RegisteredModels map[string]*resources.RegisteredModel `json:"registered_models,omitempty"`
|
||||||
|
QualityMonitors map[string]*resources.QualityMonitor `json:"quality_monitors,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UniqueResourceIdTracker struct {
|
type UniqueResourceIdTracker struct {
|
||||||
|
@ -123,6 +124,19 @@ func (r *Resources) VerifyUniqueResourceIdentifiers() (*UniqueResourceIdTracker,
|
||||||
tracker.Type[k] = "registered_model"
|
tracker.Type[k] = "registered_model"
|
||||||
tracker.ConfigPath[k] = r.RegisteredModels[k].ConfigFilePath
|
tracker.ConfigPath[k] = r.RegisteredModels[k].ConfigFilePath
|
||||||
}
|
}
|
||||||
|
for k := range r.QualityMonitors {
|
||||||
|
if _, ok := tracker.Type[k]; ok {
|
||||||
|
return tracker, fmt.Errorf("multiple resources named %s (%s at %s, %s at %s)",
|
||||||
|
k,
|
||||||
|
tracker.Type[k],
|
||||||
|
tracker.ConfigPath[k],
|
||||||
|
"quality_monitor",
|
||||||
|
r.QualityMonitors[k].ConfigFilePath,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
tracker.Type[k] = "quality_monitor"
|
||||||
|
tracker.ConfigPath[k] = r.QualityMonitors[k].ConfigFilePath
|
||||||
|
}
|
||||||
return tracker, nil
|
return tracker, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,6 +166,9 @@ func (r *Resources) allResources() []resource {
|
||||||
for k, e := range r.RegisteredModels {
|
for k, e := range r.RegisteredModels {
|
||||||
all = append(all, resource{resource_type: "registered model", resource: e, key: k})
|
all = append(all, resource{resource_type: "registered model", resource: e, key: k})
|
||||||
}
|
}
|
||||||
|
for k, e := range r.QualityMonitors {
|
||||||
|
all = append(all, resource{resource_type: "quality monitor", resource: e, key: k})
|
||||||
|
}
|
||||||
return all
|
return all
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,6 +206,9 @@ func (r *Resources) ConfigureConfigFilePath() {
|
||||||
for _, e := range r.RegisteredModels {
|
for _, e := range r.RegisteredModels {
|
||||||
e.ConfigureConfigFilePath()
|
e.ConfigureConfigFilePath()
|
||||||
}
|
}
|
||||||
|
for _, e := range r.QualityMonitors {
|
||||||
|
e.ConfigureConfigFilePath()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigResource interface {
|
type ConfigResource interface {
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
package resources
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/bundle/config/paths"
|
||||||
|
"github.com/databricks/cli/libs/log"
|
||||||
|
"github.com/databricks/databricks-sdk-go"
|
||||||
|
"github.com/databricks/databricks-sdk-go/marshal"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/catalog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QualityMonitor struct {
|
||||||
|
// Represents the Input Arguments for Terraform and will get
|
||||||
|
// converted to a HCL representation for CRUD
|
||||||
|
*catalog.CreateMonitor
|
||||||
|
|
||||||
|
// This represents the id which is the full name of the monitor
|
||||||
|
// (catalog_name.schema_name.table_name) that can be used
|
||||||
|
// as a reference in other resources. This value is returned by terraform.
|
||||||
|
ID string `json:"id,omitempty" bundle:"readonly"`
|
||||||
|
|
||||||
|
// Path to config file where the resource is defined. All bundle resources
|
||||||
|
// include this for interpolation purposes.
|
||||||
|
paths.Paths
|
||||||
|
|
||||||
|
ModifiedStatus ModifiedStatus `json:"modified_status,omitempty" bundle:"internal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *QualityMonitor) UnmarshalJSON(b []byte) error {
|
||||||
|
return marshal.Unmarshal(b, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s QualityMonitor) MarshalJSON() ([]byte, error) {
|
||||||
|
return marshal.Marshal(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *QualityMonitor) Exists(ctx context.Context, w *databricks.WorkspaceClient, id string) (bool, error) {
|
||||||
|
_, err := w.QualityMonitors.Get(ctx, catalog.GetQualityMonitorRequest{
|
||||||
|
TableName: id,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf(ctx, "quality monitor %s does not exist", id)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *QualityMonitor) TerraformResourceName() string {
|
||||||
|
return "databricks_quality_monitor"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *QualityMonitor) Validate() error {
|
||||||
|
if s == nil || !s.DynamicValue.IsValid() {
|
||||||
|
return fmt.Errorf("quality monitor is not defined")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -222,6 +222,13 @@ func BundleToTerraform(config *config.Root) *schema.Root {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for k, src := range config.Resources.QualityMonitors {
|
||||||
|
noResources = false
|
||||||
|
var dst schema.ResourceQualityMonitor
|
||||||
|
conv(src, &dst)
|
||||||
|
tfroot.Resource.QualityMonitor[k] = &dst
|
||||||
|
}
|
||||||
|
|
||||||
// We explicitly set "resource" to nil to omit it from a JSON encoding.
|
// We explicitly set "resource" to nil to omit it from a JSON encoding.
|
||||||
// This is required because the terraform CLI requires >= 1 resources defined
|
// This is required because the terraform CLI requires >= 1 resources defined
|
||||||
// if the "resource" property is used in a .tf.json file.
|
// if the "resource" property is used in a .tf.json file.
|
||||||
|
@ -365,6 +372,16 @@ func TerraformToBundle(state *resourcesState, config *config.Root) error {
|
||||||
}
|
}
|
||||||
cur.ID = instance.Attributes.ID
|
cur.ID = instance.Attributes.ID
|
||||||
config.Resources.RegisteredModels[resource.Name] = cur
|
config.Resources.RegisteredModels[resource.Name] = cur
|
||||||
|
case "databricks_quality_monitor":
|
||||||
|
if config.Resources.QualityMonitors == nil {
|
||||||
|
config.Resources.QualityMonitors = make(map[string]*resources.QualityMonitor)
|
||||||
|
}
|
||||||
|
cur := config.Resources.QualityMonitors[resource.Name]
|
||||||
|
if cur == nil {
|
||||||
|
cur = &resources.QualityMonitor{ModifiedStatus: resources.ModifiedStatusDeleted}
|
||||||
|
}
|
||||||
|
cur.ID = instance.Attributes.ID
|
||||||
|
config.Resources.QualityMonitors[resource.Name] = cur
|
||||||
case "databricks_permissions":
|
case "databricks_permissions":
|
||||||
case "databricks_grants":
|
case "databricks_grants":
|
||||||
// Ignore; no need to pull these back into the configuration.
|
// Ignore; no need to pull these back into the configuration.
|
||||||
|
@ -404,6 +421,11 @@ func TerraformToBundle(state *resourcesState, config *config.Root) error {
|
||||||
src.ModifiedStatus = resources.ModifiedStatusCreated
|
src.ModifiedStatus = resources.ModifiedStatusCreated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, src := range config.Resources.QualityMonitors {
|
||||||
|
if src.ModifiedStatus == "" && src.ID == "" {
|
||||||
|
src.ModifiedStatus = resources.ModifiedStatusCreated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -629,6 +629,14 @@ func TestTerraformToBundleEmptyLocalResources(t *testing.T) {
|
||||||
{Attributes: stateInstanceAttributes{ID: "1"}},
|
{Attributes: stateInstanceAttributes{ID: "1"}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Type: "databricks_quality_monitor",
|
||||||
|
Mode: "managed",
|
||||||
|
Name: "test_monitor",
|
||||||
|
Instances: []stateResourceInstance{
|
||||||
|
{Attributes: stateInstanceAttributes{ID: "1"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := TerraformToBundle(&tfState, &config)
|
err := TerraformToBundle(&tfState, &config)
|
||||||
|
@ -652,6 +660,9 @@ func TestTerraformToBundleEmptyLocalResources(t *testing.T) {
|
||||||
assert.Equal(t, "1", config.Resources.RegisteredModels["test_registered_model"].ID)
|
assert.Equal(t, "1", config.Resources.RegisteredModels["test_registered_model"].ID)
|
||||||
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.RegisteredModels["test_registered_model"].ModifiedStatus)
|
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.RegisteredModels["test_registered_model"].ModifiedStatus)
|
||||||
|
|
||||||
|
assert.Equal(t, "1", config.Resources.QualityMonitors["test_monitor"].ID)
|
||||||
|
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.QualityMonitors["test_monitor"].ModifiedStatus)
|
||||||
|
|
||||||
AssertFullResourceCoverage(t, &config)
|
AssertFullResourceCoverage(t, &config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -700,6 +711,13 @@ func TestTerraformToBundleEmptyRemoteResources(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
QualityMonitors: map[string]*resources.QualityMonitor{
|
||||||
|
"test_monitor": {
|
||||||
|
CreateMonitor: &catalog.CreateMonitor{
|
||||||
|
TableName: "test_monitor",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
var tfState = resourcesState{
|
var tfState = resourcesState{
|
||||||
|
@ -726,6 +744,9 @@ func TestTerraformToBundleEmptyRemoteResources(t *testing.T) {
|
||||||
assert.Equal(t, "", config.Resources.RegisteredModels["test_registered_model"].ID)
|
assert.Equal(t, "", config.Resources.RegisteredModels["test_registered_model"].ID)
|
||||||
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.RegisteredModels["test_registered_model"].ModifiedStatus)
|
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.RegisteredModels["test_registered_model"].ModifiedStatus)
|
||||||
|
|
||||||
|
assert.Equal(t, "", config.Resources.QualityMonitors["test_monitor"].ID)
|
||||||
|
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.QualityMonitors["test_monitor"].ModifiedStatus)
|
||||||
|
|
||||||
AssertFullResourceCoverage(t, &config)
|
AssertFullResourceCoverage(t, &config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -804,6 +825,18 @@ func TestTerraformToBundleModifiedResources(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
QualityMonitors: map[string]*resources.QualityMonitor{
|
||||||
|
"test_monitor": {
|
||||||
|
CreateMonitor: &catalog.CreateMonitor{
|
||||||
|
TableName: "test_monitor",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"test_monitor_new": {
|
||||||
|
CreateMonitor: &catalog.CreateMonitor{
|
||||||
|
TableName: "test_monitor_new",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
var tfState = resourcesState{
|
var tfState = resourcesState{
|
||||||
|
@ -904,6 +937,22 @@ func TestTerraformToBundleModifiedResources(t *testing.T) {
|
||||||
{Attributes: stateInstanceAttributes{ID: "2"}},
|
{Attributes: stateInstanceAttributes{ID: "2"}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Type: "databricks_quality_monitor",
|
||||||
|
Mode: "managed",
|
||||||
|
Name: "test_monitor",
|
||||||
|
Instances: []stateResourceInstance{
|
||||||
|
{Attributes: stateInstanceAttributes{ID: "test_monitor"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: "databricks_quality_monitor",
|
||||||
|
Mode: "managed",
|
||||||
|
Name: "test_monitor_old",
|
||||||
|
Instances: []stateResourceInstance{
|
||||||
|
{Attributes: stateInstanceAttributes{ID: "test_monitor_old"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
err := TerraformToBundle(&tfState, &config)
|
err := TerraformToBundle(&tfState, &config)
|
||||||
|
@ -951,6 +1000,12 @@ func TestTerraformToBundleModifiedResources(t *testing.T) {
|
||||||
assert.Equal(t, "", config.Resources.ModelServingEndpoints["test_model_serving_new"].ID)
|
assert.Equal(t, "", config.Resources.ModelServingEndpoints["test_model_serving_new"].ID)
|
||||||
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.ModelServingEndpoints["test_model_serving_new"].ModifiedStatus)
|
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.ModelServingEndpoints["test_model_serving_new"].ModifiedStatus)
|
||||||
|
|
||||||
|
assert.Equal(t, "test_monitor", config.Resources.QualityMonitors["test_monitor"].ID)
|
||||||
|
assert.Equal(t, "", config.Resources.QualityMonitors["test_monitor"].ModifiedStatus)
|
||||||
|
assert.Equal(t, "test_monitor_old", config.Resources.QualityMonitors["test_monitor_old"].ID)
|
||||||
|
assert.Equal(t, resources.ModifiedStatusDeleted, config.Resources.QualityMonitors["test_monitor_old"].ModifiedStatus)
|
||||||
|
assert.Equal(t, "", config.Resources.QualityMonitors["test_monitor_new"].ID)
|
||||||
|
assert.Equal(t, resources.ModifiedStatusCreated, config.Resources.QualityMonitors["test_monitor_new"].ModifiedStatus)
|
||||||
AssertFullResourceCoverage(t, &config)
|
AssertFullResourceCoverage(t, &config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,6 +54,8 @@ func (m *interpolateMutator) Apply(ctx context.Context, b *bundle.Bundle) diag.D
|
||||||
path = dyn.NewPath(dyn.Key("databricks_model_serving")).Append(path[2:]...)
|
path = dyn.NewPath(dyn.Key("databricks_model_serving")).Append(path[2:]...)
|
||||||
case dyn.Key("registered_models"):
|
case dyn.Key("registered_models"):
|
||||||
path = dyn.NewPath(dyn.Key("databricks_registered_model")).Append(path[2:]...)
|
path = dyn.NewPath(dyn.Key("databricks_registered_model")).Append(path[2:]...)
|
||||||
|
case dyn.Key("quality_monitors"):
|
||||||
|
path = dyn.NewPath(dyn.Key("databricks_quality_monitor")).Append(path[2:]...)
|
||||||
default:
|
default:
|
||||||
// Trigger "key not found" for unknown resource types.
|
// Trigger "key not found" for unknown resource types.
|
||||||
return dyn.GetByPath(root, path)
|
return dyn.GetByPath(root, path)
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
package tfdyn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/bundle/internal/tf/schema"
|
||||||
|
"github.com/databricks/cli/libs/dyn"
|
||||||
|
"github.com/databricks/cli/libs/dyn/convert"
|
||||||
|
"github.com/databricks/cli/libs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func convertQualityMonitorResource(ctx context.Context, vin dyn.Value) (dyn.Value, error) {
|
||||||
|
// Normalize the output value to the target schema.
|
||||||
|
vout, diags := convert.Normalize(schema.ResourceQualityMonitor{}, vin)
|
||||||
|
for _, diag := range diags {
|
||||||
|
log.Debugf(ctx, "monitor normalization diagnostic: %s", diag.Summary)
|
||||||
|
}
|
||||||
|
return vout, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type qualityMonitorConverter struct{}
|
||||||
|
|
||||||
|
func (qualityMonitorConverter) Convert(ctx context.Context, key string, vin dyn.Value, out *schema.Resources) error {
|
||||||
|
vout, err := convertQualityMonitorResource(ctx, vin)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the converted resource to the output.
|
||||||
|
out.QualityMonitor[key] = vout.AsAny()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
registerConverter("quality_monitors", qualityMonitorConverter{})
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package tfdyn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/bundle/config/resources"
|
||||||
|
"github.com/databricks/cli/bundle/internal/tf/schema"
|
||||||
|
"github.com/databricks/cli/libs/dyn"
|
||||||
|
"github.com/databricks/cli/libs/dyn/convert"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/catalog"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConvertQualityMonitor(t *testing.T) {
|
||||||
|
var src = resources.QualityMonitor{
|
||||||
|
CreateMonitor: &catalog.CreateMonitor{
|
||||||
|
TableName: "test_table_name",
|
||||||
|
AssetsDir: "assets_dir",
|
||||||
|
OutputSchemaName: "output_schema_name",
|
||||||
|
InferenceLog: &catalog.MonitorInferenceLog{
|
||||||
|
ModelIdCol: "model_id",
|
||||||
|
PredictionCol: "test_prediction_col",
|
||||||
|
ProblemType: "PROBLEM_TYPE_CLASSIFICATION",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
vin, err := convert.FromTyped(src, dyn.NilValue)
|
||||||
|
require.NoError(t, err)
|
||||||
|
ctx := context.Background()
|
||||||
|
out := schema.NewResources()
|
||||||
|
err = qualityMonitorConverter{}.Convert(ctx, "my_monitor", vin, out)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, map[string]any{
|
||||||
|
"assets_dir": "assets_dir",
|
||||||
|
"output_schema_name": "output_schema_name",
|
||||||
|
"table_name": "test_table_name",
|
||||||
|
"inference_log": map[string]any{
|
||||||
|
"model_id_col": "model_id",
|
||||||
|
"prediction_col": "test_prediction_col",
|
||||||
|
"problem_type": "PROBLEM_TYPE_CLASSIFICATION",
|
||||||
|
},
|
||||||
|
}, out.QualityMonitor["my_monitor"])
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
resources:
|
||||||
|
quality_monitors:
|
||||||
|
my_monitor:
|
||||||
|
table_name: "main.test.thing1"
|
||||||
|
assets_dir: "/Shared/provider-test/databricks_monitoring/main.test.thing1"
|
||||||
|
output_schema_name: "test"
|
||||||
|
inference_log:
|
||||||
|
granularities: ["1 day"]
|
||||||
|
timestamp_col: "timestamp"
|
||||||
|
prediction_col: "prediction"
|
||||||
|
model_id_col: "model_id"
|
||||||
|
problem_type: "PROBLEM_TYPE_REGRESSION"
|
||||||
|
|
||||||
|
targets:
|
||||||
|
development:
|
||||||
|
mode: development
|
||||||
|
resources:
|
||||||
|
quality_monitors:
|
||||||
|
my_monitor:
|
||||||
|
table_name: "main.test.dev"
|
||||||
|
|
||||||
|
staging:
|
||||||
|
resources:
|
||||||
|
quality_monitors:
|
||||||
|
my_monitor:
|
||||||
|
table_name: "main.test.staging"
|
||||||
|
output_schema_name: "staging"
|
||||||
|
|
||||||
|
production:
|
||||||
|
resources:
|
||||||
|
quality_monitors:
|
||||||
|
my_monitor:
|
||||||
|
table_name: "main.test.prod"
|
||||||
|
output_schema_name: "prod"
|
||||||
|
inference_log:
|
||||||
|
granularities: ["1 hour"]
|
||||||
|
timestamp_col: "timestamp_prod"
|
||||||
|
prediction_col: "prediction_prod"
|
||||||
|
model_id_col: "model_id_prod"
|
||||||
|
problem_type: "PROBLEM_TYPE_REGRESSION"
|
|
@ -0,0 +1,59 @@
|
||||||
|
package config_tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/databricks/cli/bundle/config"
|
||||||
|
"github.com/databricks/cli/bundle/config/resources"
|
||||||
|
"github.com/databricks/databricks-sdk-go/service/catalog"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func assertExpectedMonitor(t *testing.T, p *resources.QualityMonitor) {
|
||||||
|
assert.Equal(t, "timestamp", p.InferenceLog.TimestampCol)
|
||||||
|
assert.Equal(t, "prediction", p.InferenceLog.PredictionCol)
|
||||||
|
assert.Equal(t, "model_id", p.InferenceLog.ModelIdCol)
|
||||||
|
assert.Equal(t, catalog.MonitorInferenceLogProblemType("PROBLEM_TYPE_REGRESSION"), p.InferenceLog.ProblemType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonitorTableNames(t *testing.T) {
|
||||||
|
b := loadTarget(t, "./quality_monitor", "development")
|
||||||
|
assert.Len(t, b.Config.Resources.QualityMonitors, 1)
|
||||||
|
assert.Equal(t, b.Config.Bundle.Mode, config.Development)
|
||||||
|
|
||||||
|
p := b.Config.Resources.QualityMonitors["my_monitor"]
|
||||||
|
assert.Equal(t, "main.test.dev", p.TableName)
|
||||||
|
assert.Equal(t, "/Shared/provider-test/databricks_monitoring/main.test.thing1", p.AssetsDir)
|
||||||
|
assert.Equal(t, "test", p.OutputSchemaName)
|
||||||
|
|
||||||
|
assertExpectedMonitor(t, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonitorStaging(t *testing.T) {
|
||||||
|
b := loadTarget(t, "./quality_monitor", "staging")
|
||||||
|
assert.Len(t, b.Config.Resources.QualityMonitors, 1)
|
||||||
|
|
||||||
|
p := b.Config.Resources.QualityMonitors["my_monitor"]
|
||||||
|
assert.Equal(t, "main.test.staging", p.TableName)
|
||||||
|
assert.Equal(t, "/Shared/provider-test/databricks_monitoring/main.test.thing1", p.AssetsDir)
|
||||||
|
assert.Equal(t, "staging", p.OutputSchemaName)
|
||||||
|
|
||||||
|
assertExpectedMonitor(t, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonitorProduction(t *testing.T) {
|
||||||
|
b := loadTarget(t, "./quality_monitor", "production")
|
||||||
|
assert.Len(t, b.Config.Resources.QualityMonitors, 1)
|
||||||
|
|
||||||
|
p := b.Config.Resources.QualityMonitors["my_monitor"]
|
||||||
|
assert.Equal(t, "main.test.prod", p.TableName)
|
||||||
|
assert.Equal(t, "/Shared/provider-test/databricks_monitoring/main.test.thing1", p.AssetsDir)
|
||||||
|
assert.Equal(t, "prod", p.OutputSchemaName)
|
||||||
|
|
||||||
|
inferenceLog := p.InferenceLog
|
||||||
|
assert.Equal(t, []string{"1 day", "1 hour"}, inferenceLog.Granularities)
|
||||||
|
assert.Equal(t, "timestamp_prod", p.InferenceLog.TimestampCol)
|
||||||
|
assert.Equal(t, "prediction_prod", p.InferenceLog.PredictionCol)
|
||||||
|
assert.Equal(t, "model_id_prod", p.InferenceLog.ModelIdCol)
|
||||||
|
assert.Equal(t, catalog.MonitorInferenceLogProblemType("PROBLEM_TYPE_REGRESSION"), p.InferenceLog.ProblemType)
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/databricks/cli/libs/dyn"
|
"github.com/databricks/cli/libs/dyn"
|
||||||
|
"github.com/databricks/cli/libs/textutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// structInfo holds the type information we need to efficiently
|
// structInfo holds the type information we need to efficiently
|
||||||
|
@ -84,6 +85,14 @@ func buildStructInfo(typ reflect.Type) structInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
name, _, _ := strings.Cut(sf.Tag.Get("json"), ",")
|
name, _, _ := strings.Cut(sf.Tag.Get("json"), ",")
|
||||||
|
if typ.Name() == "QualityMonitor" && name == "-" {
|
||||||
|
urlName, _, _ := strings.Cut(sf.Tag.Get("url"), ",")
|
||||||
|
if urlName == "" || urlName == "-" {
|
||||||
|
name = textutil.CamelToSnakeCase(sf.Name)
|
||||||
|
} else {
|
||||||
|
name = urlName
|
||||||
|
}
|
||||||
|
}
|
||||||
if name == "" || name == "-" {
|
if name == "" || name == "-" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package textutil
|
||||||
|
|
||||||
|
import "unicode"
|
||||||
|
|
||||||
|
func CamelToSnakeCase(name string) string {
|
||||||
|
var out []rune = make([]rune, 0, len(name)*2)
|
||||||
|
for i, r := range name {
|
||||||
|
if i > 0 && unicode.IsUpper(r) {
|
||||||
|
out = append(out, '_')
|
||||||
|
}
|
||||||
|
out = append(out, unicode.ToLower(r))
|
||||||
|
}
|
||||||
|
return string(out)
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package textutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCamelToSnakeCase(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
input: "test",
|
||||||
|
expected: "test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "testTest",
|
||||||
|
expected: "test_test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "testTestTest",
|
||||||
|
expected: "test_test_test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "TestTest",
|
||||||
|
expected: "test_test",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "TestTestTest",
|
||||||
|
expected: "test_test_test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
output := CamelToSnakeCase(c.input)
|
||||||
|
assert.Equal(t, c.expected, output)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue