Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions acceptance/bundle/generate/alert/alert.json.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"display_name": "test alert",
"parent_path": "/Workspace/test-$UNIQUE_NAME",
"query_text": "SELECT 1\n as value",
"warehouse_id": "$TEST_DEFAULT_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"
}
}
2 changes: 2 additions & 0 deletions acceptance/bundle/generate/alert/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
bundle:
name: alert-generate
5 changes: 5 additions & 0 deletions acceptance/bundle/generate/alert/out.test.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"evaluation": {
"source": {
"name": "value"
},
"comparison_operator": "GREATER_THAN",
"threshold": {
"value": {
"double_value": 0.0
}
},
"notification": {}
},
"schedule": {
"quartz_cron_schedule": "0 0 * * * ?",
"timezone_id": "UTC"
},
"query_lines": [
"SELECT 1",
" as value"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
resources:
alerts:
test_alert:
display_name: "test alert"
warehouse_id: [TEST_DEFAULT_WAREHOUSE_ID]
file_path: ../alert/test_alert.dbalert.json
6 changes: 6 additions & 0 deletions acceptance/bundle/generate/alert/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@

>>> [CLI] workspace mkdirs /Workspace/test-[UNIQUE_NAME]

>>> [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
8 changes: 8 additions & 0 deletions acceptance/bundle/generate/alert/script
Original file line number Diff line number Diff line change
@@ -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 --source-dir out/alert --config-dir out/resource
2 changes: 2 additions & 0 deletions acceptance/bundle/generate/alert/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Cloud = true
Local = false

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

>>> errcode [CLI] bundle generate alert --existing-id f00dcafe
Error: alert with ID f00dcafe not found

Exit code: 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Test that bundle generate alert fails when the existing ID is not found
trace errcode $CLI bundle generate alert --existing-id f00dcafe
1 change: 1 addition & 0 deletions acceptance/bundle/help/bundle-generate/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions acceptance/bundle/refschema/out.fields.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions bundle/config/resources/alerts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
18 changes: 18 additions & 0 deletions bundle/generate/alert.go
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 3 additions & 0 deletions bundle/internal/schema/annotations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,9 @@ github.com/databricks/cli/bundle/config/resources.Alert:
"effective_run_as":
"description": |-
PLACEHOLDER
"file_path":
"description": |-
PLACEHOLDER
"id":
"description": |-
PLACEHOLDER
Expand Down
3 changes: 3 additions & 0 deletions bundle/schema/jsonschema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cmd/bundle/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
177 changes: 177 additions & 0 deletions cmd/bundle/generate/alert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package generate

import (
"encoding/base64"
"errors"
"fmt"
"os"
"path"
"path/filepath"

"github.com/databricks/cli/bundle/generate"
"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"
"gopkg.in/yaml.v3"
)

func NewGenerateAlertCommand() *cobra.Command {
var alertID string
var configDir string
var sourceDir string
var force bool

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:
# Generate alert configuration by ID
databricks bundle generate alert --existing-id abc123

# Specify custom directories for organization
databricks bundle generate alert --existing-id abc123 \
--key my_alert --config-dir resources --source-dir src

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

After generation, you can deploy this alert to other targets using:
databricks bundle deploy --target staging
databricks bundle deploy --target prod`,
}

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")

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`)

cmd.RunE = func(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
}

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
}

// Calculate paths
alertKey := cmd.Flag("key").Value.String()
if alertKey == "" {
alertKey = textutil.NormalizeString(alert.DisplayName)
}

// Make paths absolute if they aren't already
if !filepath.IsAbs(configDir) {
configDir = filepath.Join(b.BundleRootPath, configDir)
}
if !filepath.IsAbs(sourceDir) {
sourceDir = filepath.Join(b.BundleRootPath, sourceDir)
}

// 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
}

// Create source directory if needed
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
return err
}

// 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)
}

// Write alert definition file
if err := os.WriteFile(alertPath, alertJSON, 0o644); err != nil {
return err
}

// Convert alert to bundle configuration
v, err := generate.ConvertAlertToValue(alert, path.Join(relativeSourceDir, 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{
alertKey: v,
}),
}),
}

// Create config directory if needed
if err := os.MkdirAll(configDir, 0o755); err != nil {
return err
}

// Save configuration file
configPath := filepath.Join(configDir, alertKey+".alert.yml")
saver := yamlsaver.NewSaverWithStyle(map[string]yaml.Style{
"display_name": yaml.DoubleQuotedStyle,
})

err = saver.SaveAsYAML(result, configPath, force)
if err != nil {
return err
}

cmdio.LogString(ctx, "Alert configuration successfully saved to "+configPath)
cmdio.LogString(ctx, "Serialized alert definition to "+alertPath)

return nil
}

return cmd
}