From 20634fc54020a2a26b138cc708f37acd46989170 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 8 Dec 2025 03:43:35 +0100 Subject: [PATCH 1/6] Add support for generate for alerts --- .../bundle/generate/alert/alert.json.tmpl | 21 + .../bundle/generate/alert/databricks.yml | 2 + .../bundle/generate/alert/out.test.toml | 5 + .../alert/out/alert/test_alert.dbalert.json | 23 ++ .../alert/out/resource/test_alert.alert.yml | 6 + acceptance/bundle/generate/alert/output.txt | 6 + acceptance/bundle/generate/alert/script | 8 + .../alert_existing_id_not_found/out.test.toml | 5 + .../alert_existing_id_not_found/output.txt | 4 + .../alert_existing_id_not_found/script | 2 + bundle/config/resources/alerts.go | 4 + bundle/generate/alert.go | 18 + cmd/bundle/generate.go | 1 + cmd/bundle/generate/alert.go | 391 ++++++++++++++++++ 14 files changed, 496 insertions(+) create mode 100644 acceptance/bundle/generate/alert/alert.json.tmpl create mode 100644 acceptance/bundle/generate/alert/databricks.yml create mode 100644 acceptance/bundle/generate/alert/out.test.toml create mode 100644 acceptance/bundle/generate/alert/out/alert/test_alert.dbalert.json create mode 100644 acceptance/bundle/generate/alert/out/resource/test_alert.alert.yml create mode 100644 acceptance/bundle/generate/alert/output.txt create mode 100644 acceptance/bundle/generate/alert/script create mode 100644 acceptance/bundle/generate/alert_existing_id_not_found/out.test.toml create mode 100644 acceptance/bundle/generate/alert_existing_id_not_found/output.txt create mode 100644 acceptance/bundle/generate/alert_existing_id_not_found/script create mode 100644 bundle/generate/alert.go create mode 100644 cmd/bundle/generate/alert.go diff --git a/acceptance/bundle/generate/alert/alert.json.tmpl b/acceptance/bundle/generate/alert/alert.json.tmpl new file mode 100644 index 0000000000..ea4ef31bfa --- /dev/null +++ b/acceptance/bundle/generate/alert/alert.json.tmpl @@ -0,0 +1,21 @@ +{ + "display_name": "test alert", + "parent_path": "/Workspace/test-$UNIQUE_NAME", + "query_text": "SELECT 1 as value", + "warehouse_id": "$WAREHOUSE_ID", + "evaluation": { + "comparison_operator": "GREATER_THAN", + "source": { + "name": "value" + }, + "threshold": { + "value": { + "double_value": 0.0 + } + } + }, + "schedule": { + "quartz_cron_schedule": "0 0 * * * ?", + "timezone_id": "UTC" + } +} diff --git a/acceptance/bundle/generate/alert/databricks.yml b/acceptance/bundle/generate/alert/databricks.yml new file mode 100644 index 0000000000..8f5d694663 --- /dev/null +++ b/acceptance/bundle/generate/alert/databricks.yml @@ -0,0 +1,2 @@ +bundle: + name: alert-generate diff --git a/acceptance/bundle/generate/alert/out.test.toml b/acceptance/bundle/generate/alert/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/bundle/generate/alert/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/generate/alert/out/alert/test_alert.dbalert.json b/acceptance/bundle/generate/alert/out/alert/test_alert.dbalert.json new file mode 100644 index 0000000000..1bf5cd4e37 --- /dev/null +++ b/acceptance/bundle/generate/alert/out/alert/test_alert.dbalert.json @@ -0,0 +1,23 @@ +{ + "display_name": "test alert", + "evaluation": { + "comparison_operator": "GREATER_THAN", + "source": { + "name": "value" + }, + "threshold": { + "value": { + "double_value": 0 + } + } + }, + "id": "[UUID]", + "lifecycle_state": "ACTIVE", + "parent_path": "/Workspace/test-[UNIQUE_NAME]", + "query_text": "SELECT 1 as value", + "schedule": { + "quartz_cron_schedule": "0 0 * * * ?", + "timezone_id": "UTC" + }, + "warehouse_id": "" +} diff --git a/acceptance/bundle/generate/alert/out/resource/test_alert.alert.yml b/acceptance/bundle/generate/alert/out/resource/test_alert.alert.yml new file mode 100644 index 0000000000..7da09706b6 --- /dev/null +++ b/acceptance/bundle/generate/alert/out/resource/test_alert.alert.yml @@ -0,0 +1,6 @@ +resources: + alerts: + test_alert: + display_name: "test alert" + warehouse_id: "" + file_path: ../alert/test_alert.dbalert.json diff --git a/acceptance/bundle/generate/alert/output.txt b/acceptance/bundle/generate/alert/output.txt new file mode 100644 index 0000000000..c19129f699 --- /dev/null +++ b/acceptance/bundle/generate/alert/output.txt @@ -0,0 +1,6 @@ + +>>> [CLI] workspace mkdirs /Workspace/test-[UNIQUE_NAME] + +>>> [CLI] bundle generate alert --existing-id [UUID] --alert-dir out/alert --resource-dir out/resource +Writing alert to "out/alert/test_alert.dbalert.json" +Writing configuration to "out/resource/test_alert.alert.yml" diff --git a/acceptance/bundle/generate/alert/script b/acceptance/bundle/generate/alert/script new file mode 100644 index 0000000000..7e1d5ea13d --- /dev/null +++ b/acceptance/bundle/generate/alert/script @@ -0,0 +1,8 @@ +trace $CLI workspace mkdirs /Workspace/test-$UNIQUE_NAME + +# create an alert to import +envsubst < alert.json.tmpl > alert.json +alert_id=$($CLI alerts-v2 create-alert --json @alert.json | jq -r '.id') +rm alert.json + +trace $CLI bundle generate alert --existing-id $alert_id --alert-dir out/alert --resource-dir out/resource diff --git a/acceptance/bundle/generate/alert_existing_id_not_found/out.test.toml b/acceptance/bundle/generate/alert_existing_id_not_found/out.test.toml new file mode 100644 index 0000000000..d560f1de04 --- /dev/null +++ b/acceptance/bundle/generate/alert_existing_id_not_found/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = false + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/generate/alert_existing_id_not_found/output.txt b/acceptance/bundle/generate/alert_existing_id_not_found/output.txt new file mode 100644 index 0000000000..98d481f337 --- /dev/null +++ b/acceptance/bundle/generate/alert_existing_id_not_found/output.txt @@ -0,0 +1,4 @@ +Error: alert with ID f00dcafe not found + + +Exit code: 1 diff --git a/acceptance/bundle/generate/alert_existing_id_not_found/script b/acceptance/bundle/generate/alert_existing_id_not_found/script new file mode 100644 index 0000000000..4fc124f65b --- /dev/null +++ b/acceptance/bundle/generate/alert_existing_id_not_found/script @@ -0,0 +1,2 @@ +# Test that bundle generate alert fails when the existing ID is not found +exec $CLI bundle generate alert --existing-id f00dcafe diff --git a/bundle/config/resources/alerts.go b/bundle/config/resources/alerts.go index 35d361b556..1c2aebdf9a 100644 --- a/bundle/config/resources/alerts.go +++ b/bundle/config/resources/alerts.go @@ -15,6 +15,10 @@ type Alert struct { sql.AlertV2 //nolint AlertV2 also defines Id and URL field with the same json tag "id" and "url" Permissions []AlertPermission `json:"permissions,omitempty"` + + // FilePath points to the local `.dbalert.json` file containing the alert definition. + // This is inlined into the alert during deployment. + FilePath string `json:"file_path,omitempty"` } func (a *Alert) UnmarshalJSON(b []byte) error { diff --git a/bundle/generate/alert.go b/bundle/generate/alert.go new file mode 100644 index 0000000000..3f2428b72e --- /dev/null +++ b/bundle/generate/alert.go @@ -0,0 +1,18 @@ +package generate + +import ( + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/databricks-sdk-go/service/sql" +) + +func ConvertAlertToValue(alert *sql.AlertV2, filePath string) (dyn.Value, error) { + // The majority of fields of the alert struct are present in .dbalert.json file. + // We copy the relevant fields manually. + dv := map[string]dyn.Value{ + "display_name": dyn.NewValue(alert.DisplayName, []dyn.Location{{Line: 1}}), + "warehouse_id": dyn.NewValue(alert.WarehouseId, []dyn.Location{{Line: 2}}), + "file_path": dyn.NewValue(filePath, []dyn.Location{{Line: 3}}), + } + + return dyn.V(dv), nil +} diff --git a/cmd/bundle/generate.go b/cmd/bundle/generate.go index d282e14b5e..9caf7fa1e3 100644 --- a/cmd/bundle/generate.go +++ b/cmd/bundle/generate.go @@ -38,6 +38,7 @@ Use --bind to automatically bind the generated resource to the existing workspac cmd.AddCommand(generate.NewGenerateJobCommand()) cmd.AddCommand(generate.NewGeneratePipelineCommand()) cmd.AddCommand(generate.NewGenerateDashboardCommand()) + cmd.AddCommand(generate.NewGenerateAlertCommand()) cmd.AddCommand(generate.NewGenerateAppCommand()) cmd.PersistentFlags().StringVar(&key, "key", "", `resource key to use for the generated configuration`) return cmd diff --git a/cmd/bundle/generate/alert.go b/cmd/bundle/generate/alert.go new file mode 100644 index 0000000000..a6fab48ba5 --- /dev/null +++ b/cmd/bundle/generate/alert.go @@ -0,0 +1,391 @@ +package generate + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + + "github.com/databricks/cli/bundle" + "github.com/databricks/cli/bundle/config/engine" + "github.com/databricks/cli/bundle/generate" + "github.com/databricks/cli/bundle/phases" + "github.com/databricks/cli/bundle/resources" + "github.com/databricks/cli/bundle/statemgmt" + "github.com/databricks/cli/cmd/bundle/deployment" + "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/cmdio" + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/cli/libs/dyn/yamlsaver" + "github.com/databricks/cli/libs/logdiag" + "github.com/databricks/cli/libs/textutil" + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/service/sql" + "github.com/spf13/cobra" + "golang.org/x/exp/maps" + "gopkg.in/yaml.v3" +) + +type alert struct { + // Lookup flags for one-time generate. + existingID string + + // Lookup flag for existing bundle resource. + resource string + + // Where to write the configuration and alert representation. + resourceDir string + alertDir string + + // Force overwrite of existing files. + force bool + + // Relative path from the resource directory to the alert directory. + relativeAlertDir string + + // Command. + cmd *cobra.Command + + // Automatically bind the generated resource to the existing resource. + bind bool + + // Output and error streams. + out io.Writer + err io.Writer +} + +func (a *alert) resolveID(ctx context.Context, b *bundle.Bundle) string { + if a.existingID == "" { + logdiag.LogError(ctx, errors.New("expected --alert-id")) + return "" + } + + w := b.WorkspaceClient() + obj, err := w.AlertsV2.GetAlert(ctx, sql.GetAlertV2Request{Id: a.existingID}) + if err != nil { + if apierr.IsMissing(err) { + logdiag.LogError(ctx, fmt.Errorf("alert with ID %s not found", a.existingID)) + return "" + } + logdiag.LogError(ctx, err) + return "" + } + + return obj.Id +} + +func (a *alert) saveAlertDefinition(_ context.Context, b *bundle.Bundle, alert *sql.AlertV2, filename string) error { + // Marshal the alert to JSON. + data, err := json.Marshal(alert) + if err != nil { + return err + } + + // Unmarshal and remarshal to ensure it is formatted correctly. + data, err = remarshalJSON(data) + if err != nil { + return err + } + + // Make sure the output directory exists. + if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil { + return err + } + + // Clean the filename to ensure it is a valid path (and can be used on this OS). + filename = filepath.Clean(filename) + + // Attempt to make the path relative to the bundle root. + rel, err := filepath.Rel(b.BundleRootPath, filename) + if err != nil { + rel = filename + } + + // Verify that the file does not already exist. + info, err := os.Stat(filename) + if err == nil { + if info.IsDir() { + return fmt.Errorf("%s is a directory", rel) + } + if !a.force { + return fmt.Errorf("%s already exists. Use --force to overwrite", rel) + } + } + + fmt.Fprintf(a.out, "Writing alert to %q\n", rel) + return os.WriteFile(filename, data, 0o644) +} + +func (a *alert) saveConfiguration(ctx context.Context, b *bundle.Bundle, alert *sql.AlertV2, key string) error { + // Save alert definition to the alert directory. + alertBasename := key + ".dbalert.json" + alertPath := filepath.Join(a.alertDir, alertBasename) + err := a.saveAlertDefinition(ctx, b, alert, alertPath) + if err != nil { + return err + } + + // Synthesize resource configuration. + v, err := generate.ConvertAlertToValue(alert, path.Join(a.relativeAlertDir, alertBasename)) + if err != nil { + return err + } + + result := map[string]dyn.Value{ + "resources": dyn.V(map[string]dyn.Value{ + "alerts": dyn.V(map[string]dyn.Value{ + key: v, + }), + }), + } + + // Make sure the output directory exists. + if err := os.MkdirAll(a.resourceDir, 0o755); err != nil { + return err + } + + // Save the configuration to the resource directory. + resourcePath := filepath.Join(a.resourceDir, key+".alert.yml") + saver := yamlsaver.NewSaverWithStyle(map[string]yaml.Style{ + "display_name": yaml.DoubleQuotedStyle, + }) + + // Attempt to make the path relative to the bundle root. + rel, err := filepath.Rel(b.BundleRootPath, resourcePath) + if err != nil { + rel = resourcePath + } + + fmt.Fprintf(a.out, "Writing configuration to %q\n", rel) + err = saver.SaveAsYAML(result, resourcePath, a.force) + if err != nil { + return err + } + + return nil +} + +func (a *alert) generateForExisting(ctx context.Context, b *bundle.Bundle, alertID string) { + w := b.WorkspaceClient() + alert, err := w.AlertsV2.GetAlert(ctx, sql.GetAlertV2Request{Id: alertID}) + if err != nil { + logdiag.LogError(ctx, err) + return + } + + key := textutil.NormalizeString(alert.DisplayName) + err = a.saveConfiguration(ctx, b, alert, key) + if err != nil { + logdiag.LogError(ctx, err) + } + + if a.bind { + err = deployment.BindResource(a.cmd, key, alertID, true, false, true) + if err != nil { + logdiag.LogError(ctx, err) + return + } + cmdio.LogString(ctx, fmt.Sprintf("Successfully bound alert with an id '%s'", alertID)) + } +} + +func (a *alert) initialize(ctx context.Context, b *bundle.Bundle) { + // Make the paths absolute if they aren't already. + if !filepath.IsAbs(a.resourceDir) { + a.resourceDir = filepath.Join(b.BundleRootPath, a.resourceDir) + } + if !filepath.IsAbs(a.alertDir) { + a.alertDir = filepath.Join(b.BundleRootPath, a.alertDir) + } + + // Make sure we know how the alert path is relative to the resource path. + rel, err := filepath.Rel(a.resourceDir, a.alertDir) + if err != nil { + logdiag.LogError(ctx, err) + return + } + + a.relativeAlertDir = filepath.ToSlash(rel) +} + +func (a *alert) runForResource(ctx context.Context, b *bundle.Bundle) { + engine, err := engine.FromEnv(ctx) + if err != nil { + logdiag.LogError(ctx, err) + return + } + + phases.Initialize(ctx, b) + if logdiag.HasError(ctx) { + return + } + + ctx, stateDesc := statemgmt.PullResourcesState(ctx, b, statemgmt.AlwaysPull(true), engine) + if logdiag.HasError(ctx) { + return + } + + bundle.ApplySeqContext(ctx, b, + statemgmt.Load(stateDesc.Engine), + ) + if logdiag.HasError(ctx) { + return + } + + resource, ok := b.Config.Resources.Alerts[a.resource] + if !ok { + logdiag.LogError(ctx, fmt.Errorf("alert resource %q is not defined", a.resource)) + return + } + + if resource.FilePath == "" { + logdiag.LogError(ctx, fmt.Errorf("alert resource %q has no file path defined", a.resource)) + return + } + + // Resolve the alert ID from the resource. + alertID := resource.ID + + // Overwrite the alert at the path referenced from the resource. + alertPath := resource.FilePath + + w := b.WorkspaceClient() + alert, err := w.AlertsV2.GetAlert(ctx, sql.GetAlertV2Request{Id: alertID}) + if err != nil { + logdiag.LogError(ctx, err) + return + } + + err = a.saveAlertDefinition(ctx, b, alert, alertPath) + if err != nil { + logdiag.LogError(ctx, err) + } +} + +func (a *alert) runForExisting(ctx context.Context, b *bundle.Bundle) { + // Resolve the ID of the alert to generate configuration for. + alertID := a.resolveID(ctx, b) + if logdiag.HasError(ctx) { + return + } + + a.generateForExisting(ctx, b, alertID) +} + +func (a *alert) RunE(cmd *cobra.Command, args []string) error { + ctx := logdiag.InitContext(cmd.Context()) + cmd.SetContext(ctx) + + b := root.MustConfigureBundle(cmd) + if b == nil || logdiag.HasError(ctx) { + return root.ErrAlreadyPrinted + } + + a.initialize(ctx, b) + if logdiag.HasError(ctx) { + return root.ErrAlreadyPrinted + } + + if a.resource != "" { + a.runForResource(ctx, b) + } else { + a.runForExisting(ctx, b) + } + + if logdiag.HasError(ctx) { + return root.ErrAlreadyPrinted + } + + return nil +} + +// filterAlerts returns a filter that only includes alerts. +func filterAlerts(ref resources.Reference) bool { + return ref.Description.SingularName == "alert" +} + +// alertResourceCompletion executes to autocomplete the argument to the resource flag. +func alertResourceCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + b := root.MustConfigureBundle(cmd) + if logdiag.HasError(cmd.Context()) { + return nil, cobra.ShellCompDirectiveError + } + + if b == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + return maps.Keys(resources.Completions(b, filterAlerts)), cobra.ShellCompDirectiveNoFileComp +} + +func NewGenerateAlertCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "alert", + Short: "Generate configuration for an alert", + Long: `Generate bundle configuration for an existing Databricks alert. + +This command downloads an existing SQL alert and creates bundle files +that you can use to deploy the alert to other environments or manage it as code. + +Examples: + # Import alert by ID + databricks bundle generate alert --existing-id abc123 + + # Update existing resource configuration + databricks bundle generate alert --resource my_alert --force + +What gets generated: +- Alert configuration YAML file with settings and a reference to the alert definition +- Alert definition (.dbalert.json) file with the complete alert specification + +Sync workflow for alert development: +When developing alerts, you can modify them in the Databricks UI and sync +changes back to your bundle: + +1. Make changes to alert in the Databricks UI +2. Run: databricks bundle generate alert --resource my_alert --force +3. Commit changes to version control +4. Deploy to other environments with: databricks bundle deploy --target prod`, + } + + a := &alert{ + out: cmd.OutOrStdout(), + err: cmd.ErrOrStderr(), + } + + // Lookup flags. + cmd.Flags().StringVar(&a.existingID, "existing-id", "", `ID of the alert to generate configuration for`) + cmd.Flags().StringVar(&a.resource, "resource", "", `resource key of alert to update`) + + // Alias lookup flag that includes the resource type name. + cmd.Flags().StringVar(&a.existingID, "existing-alert-id", "", `ID of the alert to generate configuration for`) + cmd.Flags().MarkHidden("existing-alert-id") + + // Output flags. + cmd.Flags().StringVarP(&a.resourceDir, "resource-dir", "d", "resources", `directory to write the configuration to`) + cmd.Flags().StringVarP(&a.alertDir, "alert-dir", "s", "src", `directory to write the alert definition to`) + cmd.Flags().BoolVarP(&a.force, "force", "f", false, `force overwrite existing files in the output directory`) + + cmd.Flags().BoolVarP(&a.bind, "bind", "b", false, `automatically bind the generated alert config to the existing alert`) + cmd.Flags().MarkHidden("bind") + + // Exactly one of the lookup flags must be provided. + cmd.MarkFlagsOneRequired( + "existing-id", + "resource", + ) + + // Make sure the bind flag is only used with the existing-id flag. + cmd.MarkFlagsMutuallyExclusive("bind", "resource") + + // Completion for the resource flag. + cmd.RegisterFlagCompletionFunc("resource", alertResourceCompletion) + + cmd.RunE = a.RunE + a.cmd = cmd + return cmd +} From 426d44700930efbde48e954a872d4b1cc9344ccf Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 10 Dec 2025 02:27:24 +0100 Subject: [PATCH 2/6] codegen: update annotations and jsonschema --- bundle/internal/schema/annotations.yml | 3 +++ bundle/schema/jsonschema.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/bundle/internal/schema/annotations.yml b/bundle/internal/schema/annotations.yml index 29b2fd03e6..cc1a7e41c0 100644 --- a/bundle/internal/schema/annotations.yml +++ b/bundle/internal/schema/annotations.yml @@ -461,6 +461,9 @@ github.com/databricks/cli/bundle/config/resources.Alert: "effective_run_as": "description": |- PLACEHOLDER + "file_path": + "description": |- + PLACEHOLDER "id": "description": |- PLACEHOLDER diff --git a/bundle/schema/jsonschema.json b/bundle/schema/jsonschema.json index bf8dc42ac0..d83cfe0661 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -76,6 +76,9 @@ "evaluation": { "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/sql.AlertV2Evaluation" }, + "file_path": { + "$ref": "#/$defs/string" + }, "lifecycle": { "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.Lifecycle" }, From e094eb3473a322f7c721703f5a2a4f6be6d33295 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 10 Dec 2025 02:46:46 +0100 Subject: [PATCH 3/6] code: remove support for --bind --- cmd/bundle/generate/alert.go | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/cmd/bundle/generate/alert.go b/cmd/bundle/generate/alert.go index a6fab48ba5..016c1b16d8 100644 --- a/cmd/bundle/generate/alert.go +++ b/cmd/bundle/generate/alert.go @@ -16,9 +16,7 @@ import ( "github.com/databricks/cli/bundle/phases" "github.com/databricks/cli/bundle/resources" "github.com/databricks/cli/bundle/statemgmt" - "github.com/databricks/cli/cmd/bundle/deployment" "github.com/databricks/cli/cmd/root" - "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/dyn" "github.com/databricks/cli/libs/dyn/yamlsaver" "github.com/databricks/cli/libs/logdiag" @@ -50,15 +48,12 @@ type alert struct { // Command. cmd *cobra.Command - // Automatically bind the generated resource to the existing resource. - bind bool - // Output and error streams. out io.Writer err io.Writer } -func (a *alert) resolveID(ctx context.Context, b *bundle.Bundle) string { +func (a *alert) resolveFromID(ctx context.Context, b *bundle.Bundle) string { if a.existingID == "" { logdiag.LogError(ctx, errors.New("expected --alert-id")) return "" @@ -85,7 +80,7 @@ func (a *alert) saveAlertDefinition(_ context.Context, b *bundle.Bundle, alert * return err } - // Unmarshal and remarshal to ensure it is formatted correctly. + // Unmarshal and remarshal to ensure it has a stable format. data, err = remarshalJSON(data) if err != nil { return err @@ -182,15 +177,6 @@ func (a *alert) generateForExisting(ctx context.Context, b *bundle.Bundle, alert if err != nil { logdiag.LogError(ctx, err) } - - if a.bind { - err = deployment.BindResource(a.cmd, key, alertID, true, false, true) - if err != nil { - logdiag.LogError(ctx, err) - return - } - cmdio.LogString(ctx, fmt.Sprintf("Successfully bound alert with an id '%s'", alertID)) - } } func (a *alert) initialize(ctx context.Context, b *bundle.Bundle) { @@ -268,7 +254,7 @@ func (a *alert) runForResource(ctx context.Context, b *bundle.Bundle) { func (a *alert) runForExisting(ctx context.Context, b *bundle.Bundle) { // Resolve the ID of the alert to generate configuration for. - alertID := a.resolveID(ctx, b) + alertID := a.resolveFromID(ctx, b) if logdiag.HasError(ctx) { return } @@ -332,7 +318,7 @@ This command downloads an existing SQL alert and creates bundle files that you can use to deploy the alert to other environments or manage it as code. Examples: - # Import alert by ID + # Generate alert configuration by ID databricks bundle generate alert --existing-id abc123 # Update existing resource configuration @@ -370,18 +356,12 @@ changes back to your bundle: cmd.Flags().StringVarP(&a.alertDir, "alert-dir", "s", "src", `directory to write the alert definition to`) cmd.Flags().BoolVarP(&a.force, "force", "f", false, `force overwrite existing files in the output directory`) - cmd.Flags().BoolVarP(&a.bind, "bind", "b", false, `automatically bind the generated alert config to the existing alert`) - cmd.Flags().MarkHidden("bind") - // Exactly one of the lookup flags must be provided. cmd.MarkFlagsOneRequired( "existing-id", "resource", ) - // Make sure the bind flag is only used with the existing-id flag. - cmd.MarkFlagsMutuallyExclusive("bind", "resource") - // Completion for the resource flag. cmd.RegisterFlagCompletionFunc("resource", alertResourceCompletion) From 4d6170d4f7e687654a5b8570077aef4076d0ced4 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 10 Dec 2025 02:49:39 +0100 Subject: [PATCH 4/6] generate: acceptance tests golden --- acceptance/bundle/help/bundle-generate/output.txt | 1 + acceptance/bundle/refschema/out.fields.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/acceptance/bundle/help/bundle-generate/output.txt b/acceptance/bundle/help/bundle-generate/output.txt index 9dedd80b39..97e8667ac7 100644 --- a/acceptance/bundle/help/bundle-generate/output.txt +++ b/acceptance/bundle/help/bundle-generate/output.txt @@ -27,6 +27,7 @@ Usage: databricks bundle generate [command] Available Commands: + alert Generate configuration for an alert app Generate bundle configuration for a Databricks app dashboard Generate configuration for a dashboard job Generate bundle configuration for a job diff --git a/acceptance/bundle/refschema/out.fields.txt b/acceptance/bundle/refschema/out.fields.txt index 2394ea7989..5f932d62c1 100644 --- a/acceptance/bundle/refschema/out.fields.txt +++ b/acceptance/bundle/refschema/out.fields.txt @@ -30,6 +30,7 @@ resources.alerts.*.evaluation.threshold.value *sql.AlertV2OperandValue ALL resources.alerts.*.evaluation.threshold.value.bool_value bool ALL resources.alerts.*.evaluation.threshold.value.double_value float64 ALL resources.alerts.*.evaluation.threshold.value.string_value string ALL +resources.alerts.*.file_path string INPUT resources.alerts.*.id string ALL resources.alerts.*.lifecycle resources.Lifecycle INPUT resources.alerts.*.lifecycle.prevent_destroy bool INPUT From d8ea0742ff7381f0bedbd09b06e33928b52755a5 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 10 Dec 2025 03:56:24 +0100 Subject: [PATCH 5/6] update --- .../bundle/generate/alert/alert.json.tmpl | 4 +- .../bundle/generate/alert/out.test.toml | 2 +- .../alert/out/alert/test_alert.dbalert.json | 17 +- .../alert/out/resource/test_alert.alert.yml | 2 +- acceptance/bundle/generate/alert/output.txt | 6 +- acceptance/bundle/generate/alert/script | 2 +- acceptance/bundle/generate/alert/test.toml | 2 + .../alert_existing_id_not_found/output.txt | 3 +- .../alert_existing_id_not_found/script | 2 +- cmd/bundle/generate/alert.go | 444 +++++------------- 10 files changed, 146 insertions(+), 338 deletions(-) create mode 100644 acceptance/bundle/generate/alert/test.toml diff --git a/acceptance/bundle/generate/alert/alert.json.tmpl b/acceptance/bundle/generate/alert/alert.json.tmpl index ea4ef31bfa..6aa6902422 100644 --- a/acceptance/bundle/generate/alert/alert.json.tmpl +++ b/acceptance/bundle/generate/alert/alert.json.tmpl @@ -1,8 +1,8 @@ { "display_name": "test alert", "parent_path": "/Workspace/test-$UNIQUE_NAME", - "query_text": "SELECT 1 as value", - "warehouse_id": "$WAREHOUSE_ID", + "query_text": "SELECT 1\n as value", + "warehouse_id": "$TEST_DEFAULT_WAREHOUSE_ID", "evaluation": { "comparison_operator": "GREATER_THAN", "source": { diff --git a/acceptance/bundle/generate/alert/out.test.toml b/acceptance/bundle/generate/alert/out.test.toml index d560f1de04..01ed6822af 100644 --- a/acceptance/bundle/generate/alert/out.test.toml +++ b/acceptance/bundle/generate/alert/out.test.toml @@ -1,5 +1,5 @@ Local = true -Cloud = false +Cloud = true [EnvMatrix] DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/generate/alert/out/alert/test_alert.dbalert.json b/acceptance/bundle/generate/alert/out/alert/test_alert.dbalert.json index 1bf5cd4e37..a4ea8a85c4 100644 --- a/acceptance/bundle/generate/alert/out/alert/test_alert.dbalert.json +++ b/acceptance/bundle/generate/alert/out/alert/test_alert.dbalert.json @@ -1,23 +1,22 @@ { - "display_name": "test alert", "evaluation": { - "comparison_operator": "GREATER_THAN", "source": { "name": "value" }, + "comparison_operator": "GREATER_THAN", "threshold": { "value": { - "double_value": 0 + "double_value": 0.0 } - } + }, + "notification": {} }, - "id": "[UUID]", - "lifecycle_state": "ACTIVE", - "parent_path": "/Workspace/test-[UNIQUE_NAME]", - "query_text": "SELECT 1 as value", "schedule": { "quartz_cron_schedule": "0 0 * * * ?", "timezone_id": "UTC" }, - "warehouse_id": "" + "query_lines": [ + "SELECT 1", + " as value" + ] } diff --git a/acceptance/bundle/generate/alert/out/resource/test_alert.alert.yml b/acceptance/bundle/generate/alert/out/resource/test_alert.alert.yml index 7da09706b6..0a4dced932 100644 --- a/acceptance/bundle/generate/alert/out/resource/test_alert.alert.yml +++ b/acceptance/bundle/generate/alert/out/resource/test_alert.alert.yml @@ -2,5 +2,5 @@ resources: alerts: test_alert: display_name: "test alert" - warehouse_id: "" + warehouse_id: [TEST_DEFAULT_WAREHOUSE_ID] file_path: ../alert/test_alert.dbalert.json diff --git a/acceptance/bundle/generate/alert/output.txt b/acceptance/bundle/generate/alert/output.txt index c19129f699..9d8a9f77eb 100644 --- a/acceptance/bundle/generate/alert/output.txt +++ b/acceptance/bundle/generate/alert/output.txt @@ -1,6 +1,6 @@ >>> [CLI] workspace mkdirs /Workspace/test-[UNIQUE_NAME] ->>> [CLI] bundle generate alert --existing-id [UUID] --alert-dir out/alert --resource-dir out/resource -Writing alert to "out/alert/test_alert.dbalert.json" -Writing configuration to "out/resource/test_alert.alert.yml" +>>> [CLI] bundle generate alert --existing-id [NUMID] --source-dir out/alert --config-dir out/resource +Alert configuration successfully saved to [TEST_TMP_DIR]/out/resource/test_alert.alert.yml +Serialized alert definition to [TEST_TMP_DIR]/out/alert/test_alert.dbalert.json diff --git a/acceptance/bundle/generate/alert/script b/acceptance/bundle/generate/alert/script index 7e1d5ea13d..ae40309f98 100644 --- a/acceptance/bundle/generate/alert/script +++ b/acceptance/bundle/generate/alert/script @@ -5,4 +5,4 @@ envsubst < alert.json.tmpl > alert.json alert_id=$($CLI alerts-v2 create-alert --json @alert.json | jq -r '.id') rm alert.json -trace $CLI bundle generate alert --existing-id $alert_id --alert-dir out/alert --resource-dir out/resource +trace $CLI bundle generate alert --existing-id $alert_id --source-dir out/alert --config-dir out/resource diff --git a/acceptance/bundle/generate/alert/test.toml b/acceptance/bundle/generate/alert/test.toml new file mode 100644 index 0000000000..1c1fa982aa --- /dev/null +++ b/acceptance/bundle/generate/alert/test.toml @@ -0,0 +1,2 @@ +Cloud = true +Local = false diff --git a/acceptance/bundle/generate/alert_existing_id_not_found/output.txt b/acceptance/bundle/generate/alert_existing_id_not_found/output.txt index 98d481f337..3f841a9279 100644 --- a/acceptance/bundle/generate/alert_existing_id_not_found/output.txt +++ b/acceptance/bundle/generate/alert_existing_id_not_found/output.txt @@ -1,4 +1,5 @@ -Error: alert with ID f00dcafe not found +>>> errcode [CLI] bundle generate alert --existing-id f00dcafe +Error: alert with ID f00dcafe not found Exit code: 1 diff --git a/acceptance/bundle/generate/alert_existing_id_not_found/script b/acceptance/bundle/generate/alert_existing_id_not_found/script index 4fc124f65b..85edf127c6 100644 --- a/acceptance/bundle/generate/alert_existing_id_not_found/script +++ b/acceptance/bundle/generate/alert_existing_id_not_found/script @@ -1,2 +1,2 @@ # Test that bundle generate alert fails when the existing ID is not found -exec $CLI bundle generate alert --existing-id f00dcafe +trace errcode $CLI bundle generate alert --existing-id f00dcafe diff --git a/cmd/bundle/generate/alert.go b/cmd/bundle/generate/alert.go index 016c1b16d8..acd0c080fc 100644 --- a/cmd/bundle/generate/alert.go +++ b/cmd/bundle/generate/alert.go @@ -1,371 +1,177 @@ package generate import ( - "context" - "encoding/json" + "encoding/base64" "errors" "fmt" - "io" "os" "path" "path/filepath" - "github.com/databricks/cli/bundle" - "github.com/databricks/cli/bundle/config/engine" "github.com/databricks/cli/bundle/generate" - "github.com/databricks/cli/bundle/phases" - "github.com/databricks/cli/bundle/resources" - "github.com/databricks/cli/bundle/statemgmt" "github.com/databricks/cli/cmd/root" + "github.com/databricks/cli/libs/cmdio" "github.com/databricks/cli/libs/dyn" "github.com/databricks/cli/libs/dyn/yamlsaver" "github.com/databricks/cli/libs/logdiag" "github.com/databricks/cli/libs/textutil" "github.com/databricks/databricks-sdk-go/apierr" "github.com/databricks/databricks-sdk-go/service/sql" + "github.com/databricks/databricks-sdk-go/service/workspace" "github.com/spf13/cobra" - "golang.org/x/exp/maps" "gopkg.in/yaml.v3" ) -type alert struct { - // Lookup flags for one-time generate. - existingID string - - // Lookup flag for existing bundle resource. - resource string - - // Where to write the configuration and alert representation. - resourceDir string - alertDir string +func NewGenerateAlertCommand() *cobra.Command { + var alertID string + var configDir string + var sourceDir string + var force bool - // Force overwrite of existing files. - force bool + cmd := &cobra.Command{ + Use: "alert", + Short: "Generate configuration for an alert", + Long: `Generate bundle configuration for an existing Databricks alert. - // Relative path from the resource directory to the alert directory. - relativeAlertDir string +This command downloads an existing SQL alert and creates bundle files +that you can use to deploy the alert to other environments or manage it as code. - // Command. - cmd *cobra.Command +Examples: + # Generate alert configuration by ID + databricks bundle generate alert --existing-id abc123 - // Output and error streams. - out io.Writer - err io.Writer -} + # Specify custom directories for organization + databricks bundle generate alert --existing-id abc123 \ + --key my_alert --config-dir resources --source-dir src -func (a *alert) resolveFromID(ctx context.Context, b *bundle.Bundle) string { - if a.existingID == "" { - logdiag.LogError(ctx, errors.New("expected --alert-id")) - return "" - } +What gets generated: +- Alert configuration YAML file with settings and a reference to the alert definition +- Alert definition (.dbalert.json) file with the complete alert specification - w := b.WorkspaceClient() - obj, err := w.AlertsV2.GetAlert(ctx, sql.GetAlertV2Request{Id: a.existingID}) - if err != nil { - if apierr.IsMissing(err) { - logdiag.LogError(ctx, fmt.Errorf("alert with ID %s not found", a.existingID)) - return "" - } - logdiag.LogError(ctx, err) - return "" +After generation, you can deploy this alert to other targets using: + databricks bundle deploy --target staging + databricks bundle deploy --target prod`, } - return obj.Id -} + cmd.Flags().StringVar(&alertID, "existing-id", "", `ID of the alert to generate configuration for`) + cmd.Flags().StringVar(&alertID, "existing-alert-id", "", `ID of the alert to generate configuration for`) + cmd.Flags().MarkHidden("existing-alert-id") + cmd.MarkFlagRequired("existing-id") -func (a *alert) saveAlertDefinition(_ context.Context, b *bundle.Bundle, alert *sql.AlertV2, filename string) error { - // Marshal the alert to JSON. - data, err := json.Marshal(alert) - if err != nil { - return err - } + cmd.Flags().StringVarP(&configDir, "config-dir", "d", "resources", `directory to write the configuration to`) + cmd.Flags().StringVarP(&sourceDir, "source-dir", "s", "src", `directory to write the alert definition to`) + cmd.Flags().BoolVarP(&force, "force", "f", false, `force overwrite existing files in the output directory`) - // Unmarshal and remarshal to ensure it has a stable format. - data, err = remarshalJSON(data) - if err != nil { - return err - } + cmd.RunE = func(cmd *cobra.Command, args []string) error { + ctx := logdiag.InitContext(cmd.Context()) + cmd.SetContext(ctx) - // Make sure the output directory exists. - if err := os.MkdirAll(filepath.Dir(filename), 0o755); err != nil { - return err - } + b := root.MustConfigureBundle(cmd) + if b == nil || logdiag.HasError(ctx) { + return root.ErrAlreadyPrinted + } - // Clean the filename to ensure it is a valid path (and can be used on this OS). - filename = filepath.Clean(filename) + w := b.WorkspaceClient() + + // Get alert from Databricks + alert, err := w.AlertsV2.GetAlert(ctx, sql.GetAlertV2Request{Id: alertID}) + if err != nil { + // Check if it's a not found error to provide a better message + var apiErr *apierr.APIError + if errors.As(err, &apiErr) && apiErr.StatusCode == 404 { + return fmt.Errorf("alert with ID %s not found", alertID) + } + return err + } - // Attempt to make the path relative to the bundle root. - rel, err := filepath.Rel(b.BundleRootPath, filename) - if err != nil { - rel = filename - } + // Calculate paths + alertKey := cmd.Flag("key").Value.String() + if alertKey == "" { + alertKey = textutil.NormalizeString(alert.DisplayName) + } - // Verify that the file does not already exist. - info, err := os.Stat(filename) - if err == nil { - if info.IsDir() { - return fmt.Errorf("%s is a directory", rel) + // Make paths absolute if they aren't already + if !filepath.IsAbs(configDir) { + configDir = filepath.Join(b.BundleRootPath, configDir) } - if !a.force { - return fmt.Errorf("%s already exists. Use --force to overwrite", rel) + if !filepath.IsAbs(sourceDir) { + sourceDir = filepath.Join(b.BundleRootPath, sourceDir) } - } - - fmt.Fprintf(a.out, "Writing alert to %q\n", rel) - return os.WriteFile(filename, data, 0o644) -} - -func (a *alert) saveConfiguration(ctx context.Context, b *bundle.Bundle, alert *sql.AlertV2, key string) error { - // Save alert definition to the alert directory. - alertBasename := key + ".dbalert.json" - alertPath := filepath.Join(a.alertDir, alertBasename) - err := a.saveAlertDefinition(ctx, b, alert, alertPath) - if err != nil { - return err - } - - // Synthesize resource configuration. - v, err := generate.ConvertAlertToValue(alert, path.Join(a.relativeAlertDir, alertBasename)) - if err != nil { - return err - } - - result := map[string]dyn.Value{ - "resources": dyn.V(map[string]dyn.Value{ - "alerts": dyn.V(map[string]dyn.Value{ - key: v, - }), - }), - } - - // Make sure the output directory exists. - if err := os.MkdirAll(a.resourceDir, 0o755); err != nil { - return err - } - - // Save the configuration to the resource directory. - resourcePath := filepath.Join(a.resourceDir, key+".alert.yml") - saver := yamlsaver.NewSaverWithStyle(map[string]yaml.Style{ - "display_name": yaml.DoubleQuotedStyle, - }) - - // Attempt to make the path relative to the bundle root. - rel, err := filepath.Rel(b.BundleRootPath, resourcePath) - if err != nil { - rel = resourcePath - } - - fmt.Fprintf(a.out, "Writing configuration to %q\n", rel) - err = saver.SaveAsYAML(result, resourcePath, a.force) - if err != nil { - return err - } - - return nil -} - -func (a *alert) generateForExisting(ctx context.Context, b *bundle.Bundle, alertID string) { - w := b.WorkspaceClient() - alert, err := w.AlertsV2.GetAlert(ctx, sql.GetAlertV2Request{Id: alertID}) - if err != nil { - logdiag.LogError(ctx, err) - return - } - - key := textutil.NormalizeString(alert.DisplayName) - err = a.saveConfiguration(ctx, b, alert, key) - if err != nil { - logdiag.LogError(ctx, err) - } -} - -func (a *alert) initialize(ctx context.Context, b *bundle.Bundle) { - // Make the paths absolute if they aren't already. - if !filepath.IsAbs(a.resourceDir) { - a.resourceDir = filepath.Join(b.BundleRootPath, a.resourceDir) - } - if !filepath.IsAbs(a.alertDir) { - a.alertDir = filepath.Join(b.BundleRootPath, a.alertDir) - } - - // Make sure we know how the alert path is relative to the resource path. - rel, err := filepath.Rel(a.resourceDir, a.alertDir) - if err != nil { - logdiag.LogError(ctx, err) - return - } - - a.relativeAlertDir = filepath.ToSlash(rel) -} - -func (a *alert) runForResource(ctx context.Context, b *bundle.Bundle) { - engine, err := engine.FromEnv(ctx) - if err != nil { - logdiag.LogError(ctx, err) - return - } - - phases.Initialize(ctx, b) - if logdiag.HasError(ctx) { - return - } - - ctx, stateDesc := statemgmt.PullResourcesState(ctx, b, statemgmt.AlwaysPull(true), engine) - if logdiag.HasError(ctx) { - return - } - bundle.ApplySeqContext(ctx, b, - statemgmt.Load(stateDesc.Engine), - ) - if logdiag.HasError(ctx) { - return - } - - resource, ok := b.Config.Resources.Alerts[a.resource] - if !ok { - logdiag.LogError(ctx, fmt.Errorf("alert resource %q is not defined", a.resource)) - return - } - - if resource.FilePath == "" { - logdiag.LogError(ctx, fmt.Errorf("alert resource %q has no file path defined", a.resource)) - return - } - - // Resolve the alert ID from the resource. - alertID := resource.ID - - // Overwrite the alert at the path referenced from the resource. - alertPath := resource.FilePath - - w := b.WorkspaceClient() - alert, err := w.AlertsV2.GetAlert(ctx, sql.GetAlertV2Request{Id: alertID}) - if err != nil { - logdiag.LogError(ctx, err) - return - } - - err = a.saveAlertDefinition(ctx, b, alert, alertPath) - if err != nil { - logdiag.LogError(ctx, err) - } -} - -func (a *alert) runForExisting(ctx context.Context, b *bundle.Bundle) { - // Resolve the ID of the alert to generate configuration for. - alertID := a.resolveFromID(ctx, b) - if logdiag.HasError(ctx) { - return - } - - a.generateForExisting(ctx, b, alertID) -} - -func (a *alert) RunE(cmd *cobra.Command, args []string) error { - ctx := logdiag.InitContext(cmd.Context()) - cmd.SetContext(ctx) - - b := root.MustConfigureBundle(cmd) - if b == nil || logdiag.HasError(ctx) { - return root.ErrAlreadyPrinted - } - - a.initialize(ctx, b) - if logdiag.HasError(ctx) { - return root.ErrAlreadyPrinted - } - - if a.resource != "" { - a.runForResource(ctx, b) - } else { - a.runForExisting(ctx, b) - } - - if logdiag.HasError(ctx) { - return root.ErrAlreadyPrinted - } - - return nil -} - -// filterAlerts returns a filter that only includes alerts. -func filterAlerts(ref resources.Reference) bool { - return ref.Description.SingularName == "alert" -} - -// alertResourceCompletion executes to autocomplete the argument to the resource flag. -func alertResourceCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - b := root.MustConfigureBundle(cmd) - if logdiag.HasError(cmd.Context()) { - return nil, cobra.ShellCompDirectiveError - } + // Calculate relative path from config dir to source dir + relativeSourceDir, err := filepath.Rel(configDir, sourceDir) + if err != nil { + return err + } + relativeSourceDir = filepath.ToSlash(relativeSourceDir) + + // Save alert definition to source directory + alertBasename := alertKey + ".dbalert.json" + alertPath := filepath.Join(sourceDir, alertBasename) + + // remote alert path + remoteAlertPath := path.Join(alert.ParentPath, alert.DisplayName+".dbalert.json") + resp, err := w.Workspace.Export(ctx, workspace.ExportRequest{ + Path: remoteAlertPath, + }) + if err != nil { + return err + } + alertJSON, err := base64.StdEncoding.DecodeString(resp.Content) + if err != nil { + return err + } - if b == nil { - return nil, cobra.ShellCompDirectiveNoFileComp - } + // Create source directory if needed + if err := os.MkdirAll(sourceDir, 0o755); err != nil { + return err + } - return maps.Keys(resources.Completions(b, filterAlerts)), cobra.ShellCompDirectiveNoFileComp -} + // Check if file exists and force flag + if _, err := os.Stat(alertPath); err == nil && !force { + return fmt.Errorf("%s already exists. Use --force to overwrite", alertPath) + } -func NewGenerateAlertCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "alert", - Short: "Generate configuration for an alert", - Long: `Generate bundle configuration for an existing Databricks alert. + // Write alert definition file + if err := os.WriteFile(alertPath, alertJSON, 0o644); err != nil { + return err + } -This command downloads an existing SQL alert and creates bundle files -that you can use to deploy the alert to other environments or manage it as code. + // Convert alert to bundle configuration + v, err := generate.ConvertAlertToValue(alert, path.Join(relativeSourceDir, alertBasename)) + if err != nil { + return err + } -Examples: - # Generate alert configuration by ID - databricks bundle generate alert --existing-id abc123 + result := map[string]dyn.Value{ + "resources": dyn.V(map[string]dyn.Value{ + "alerts": dyn.V(map[string]dyn.Value{ + alertKey: v, + }), + }), + } - # Update existing resource configuration - databricks bundle generate alert --resource my_alert --force + // Create config directory if needed + if err := os.MkdirAll(configDir, 0o755); err != nil { + return err + } -What gets generated: -- Alert configuration YAML file with settings and a reference to the alert definition -- Alert definition (.dbalert.json) file with the complete alert specification + // Save configuration file + configPath := filepath.Join(configDir, alertKey+".alert.yml") + saver := yamlsaver.NewSaverWithStyle(map[string]yaml.Style{ + "display_name": yaml.DoubleQuotedStyle, + }) -Sync workflow for alert development: -When developing alerts, you can modify them in the Databricks UI and sync -changes back to your bundle: + err = saver.SaveAsYAML(result, configPath, force) + if err != nil { + return err + } -1. Make changes to alert in the Databricks UI -2. Run: databricks bundle generate alert --resource my_alert --force -3. Commit changes to version control -4. Deploy to other environments with: databricks bundle deploy --target prod`, - } + cmdio.LogString(ctx, fmt.Sprintf("Alert configuration successfully saved to %s", configPath)) + cmdio.LogString(ctx, fmt.Sprintf("Serialized alert definition to %s", alertPath)) - a := &alert{ - out: cmd.OutOrStdout(), - err: cmd.ErrOrStderr(), + return nil } - // Lookup flags. - cmd.Flags().StringVar(&a.existingID, "existing-id", "", `ID of the alert to generate configuration for`) - cmd.Flags().StringVar(&a.resource, "resource", "", `resource key of alert to update`) - - // Alias lookup flag that includes the resource type name. - cmd.Flags().StringVar(&a.existingID, "existing-alert-id", "", `ID of the alert to generate configuration for`) - cmd.Flags().MarkHidden("existing-alert-id") - - // Output flags. - cmd.Flags().StringVarP(&a.resourceDir, "resource-dir", "d", "resources", `directory to write the configuration to`) - cmd.Flags().StringVarP(&a.alertDir, "alert-dir", "s", "src", `directory to write the alert definition to`) - cmd.Flags().BoolVarP(&a.force, "force", "f", false, `force overwrite existing files in the output directory`) - - // Exactly one of the lookup flags must be provided. - cmd.MarkFlagsOneRequired( - "existing-id", - "resource", - ) - - // Completion for the resource flag. - cmd.RegisterFlagCompletionFunc("resource", alertResourceCompletion) - - cmd.RunE = a.RunE - a.cmd = cmd return cmd } From 734fe56af53e16de28460be70cb775d40e3e83a5 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 10 Dec 2025 04:00:41 +0100 Subject: [PATCH 6/6] lint: perfprintf --- cmd/bundle/generate/alert.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/bundle/generate/alert.go b/cmd/bundle/generate/alert.go index acd0c080fc..c2881affc1 100644 --- a/cmd/bundle/generate/alert.go +++ b/cmd/bundle/generate/alert.go @@ -167,8 +167,8 @@ After generation, you can deploy this alert to other targets using: return err } - cmdio.LogString(ctx, fmt.Sprintf("Alert configuration successfully saved to %s", configPath)) - cmdio.LogString(ctx, fmt.Sprintf("Serialized alert definition to %s", alertPath)) + cmdio.LogString(ctx, "Alert configuration successfully saved to "+configPath) + cmdio.LogString(ctx, "Serialized alert definition to "+alertPath) return nil }