-
Notifications
You must be signed in to change notification settings - Fork 125
Add support for generate for alerts #4108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
20634fc
905c2d8
426d447
e094eb3
4d6170d
d8ea074
734fe56
568391d
70486d8
88767b0
7a5c173
c6a9daf
834b5b0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| bundle: | ||
| name: alert-generate |
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 |
| 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 |
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| Cloud = true | ||
| Local = false | ||
|
|
||
| [Env] | ||
| # MSYS2 automatically converts absolute paths like /Users/$username/$UNIQUE_NAME to | ||
| # C:/Program Files/Git/Users/$username/UNIQUE_NAME before passing it to the CLI | ||
| # Setting this environment variable prevents that conversion on windows. | ||
| MSYS_NO_PATHCONV = "1" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| bundle: | ||
| name: test-alert-existing-id-not-found |
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 |
| 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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| 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.MarkFlagRequired("existing-id") | ||
|
|
||
| // Alias lookup flag that includes the resource type name. | ||
| // Included for symmetry with the other generate commands, but we prefer the shorter flag. | ||
| cmd.Flags().StringVar(&alertID, "existing-alert-id", "", `ID of the alert to generate configuration for`) | ||
| cmd.Flags().MarkHidden("existing-alert-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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could libs/diag/sdk_error.go be useful here to show error in full? or perhaps somewhere at the caller side?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea but I don't think we should do it here. We can solve this in general by modifying the main function and logging the API error details everytime we see an |
||
| } | ||
|
|
||
| // 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 | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what the difference between these two?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have something similar in dashboards as well, so to keep things consistent:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add a comment clarifying this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we have a preferred way then the other is deprecated? In that case, we can skip it for new resources?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the goal was symmetry (cc: @pietern ). I don't mind it either way. The flag is hidden so users will not be encouraged to use it.