Skip to content
Merged
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
2 changes: 1 addition & 1 deletion acceptance/bundle/resources/alerts/with_file/out.test.toml

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

2 changes: 1 addition & 1 deletion acceptance/bundle/resources/alerts/with_file/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Deployment complete!
=== assert that the uploaded alert file is the same as the local one
>>> [CLI] workspace export /Workspace/Users/[USERNAME]/.bundle/alerts-with-file-[UNIQUE_NAME]/default/resources/myalert.dbalert.json

>>> diff exported_alert.dbalert.json alert.dbalert.json --strip-trailing-cr
>>> diff local_normalized.json exported_normalized.json

>>> [CLI] alerts-v2 get-alert [ALERT_ID]
{
Expand Down
10 changes: 8 additions & 2 deletions acceptance/bundle/resources/alerts/with_file/script
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@ trace $CLI alerts-v2 get-alert $alert_id | jq '{display_name, lifecycle_state, c
alert_path=$($CLI bundle summary --output json | jq -r '.resources.alerts.myalert.parent_path')/myalert.dbalert.json
title "assert that the uploaded alert file is the same as the local one"
trace $CLI workspace export $alert_path > exported_alert.dbalert.json
trace diff exported_alert.dbalert.json alert.dbalert.json --strip-trailing-cr
rm exported_alert.dbalert.json
# The backend re-serializes the alert file when materializing it in the workspace
# (different key order, and numbers like 2.0 vs 2), so compare canonically with
# sorted keys and normalized numbers rather than byte-for-byte.
normalize_json() { jq -S 'walk(if type == "number" then . + 0 else . end)' "$1"; }
normalize_json exported_alert.dbalert.json > exported_normalized.json
normalize_json alert.dbalert.json > local_normalized.json
trace diff local_normalized.json exported_normalized.json
rm exported_alert.dbalert.json exported_normalized.json local_normalized.json

trace $CLI alerts-v2 get-alert $alert_id | jq '{display_name, lifecycle_state}'

Expand Down
2 changes: 1 addition & 1 deletion acceptance/bundle/resources/alerts/with_file/test.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Local = false
Local = true
Cloud = true
RecordRequests = false
Ignore = [".databricks"]
Expand Down
82 changes: 82 additions & 0 deletions libs/testserver/alerts.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,83 @@ import (
"encoding/json"
"fmt"
"net/http"
"path"
"strings"

"github.com/databricks/databricks-sdk-go/service/sql"
"github.com/databricks/databricks-sdk-go/service/workspace"
)

// alertFile mirrors the schema of a .dbalert.json file. It is the inverse of
// bundle/config/mutator/load_dbalert_files.go: API-only fields (display_name,
// warehouse_id) are dropped and the joined query_text/custom_description are
// split back into lines. The field order matches what the backend materializes.
type alertFile struct {
CustomSummary string `json:"custom_summary,omitempty"`
Evaluation *sql.AlertV2Evaluation `json:"evaluation,omitempty"`
Schedule *sql.CronSchedule `json:"schedule,omitempty"`
QueryLines []string `json:"query_lines,omitempty"`
CustomDescriptionLines []string `json:"custom_description_lines,omitempty"`
}

// alertFilePath returns the workspace path of the .dbalert.json file the backend
// materializes for an alert: <parent_path>/<display_name>.dbalert.json.
func alertFilePath(alert sql.AlertV2) string {
return path.Join(alert.ParentPath, alert.DisplayName+".dbalert.json")
}

// splitLines reverses the line-joining done in load_dbalert_files.go: each line
// is terminated by "\n", so splitting drops the trailing empty element.
func splitLines(s string) []string {
if s == "" {
return nil
}
return strings.Split(strings.TrimSuffix(s, "\n"), "\n")
}

// writeAlertFile materializes the .dbalert.json file for an alert. On real cloud
// the backend writes this file as a side effect of alert creation/update, which
// the `workspace export` round-trip and `bundle generate alert` rely on.
func (s *FakeWorkspace) writeAlertFile(alert sql.AlertV2) error {
if alert.ParentPath == "" || alert.DisplayName == "" {
return nil
}

evaluation := alert.Evaluation
if evaluation.Notification != nil {
// The backend always serializes notify_on_ok in the file, even when
// false; the SDK marshaler would otherwise drop the zero value.
notification := *evaluation.Notification
notification.ForceSendFields = append(notification.ForceSendFields, "NotifyOnOk")
evaluation.Notification = &notification
}

af := alertFile{
CustomSummary: alert.CustomSummary,
Evaluation: &evaluation,
Schedule: &alert.Schedule,
QueryLines: splitLines(alert.QueryText),
CustomDescriptionLines: splitLines(alert.CustomDescription),
}

data, err := json.MarshalIndent(af, "", " ")
if err != nil {
return err
}
data = append(data, '\n')

filePath := alertFilePath(alert)
s.files[filePath] = FileEntry{
Info: workspace.ObjectInfo{
ObjectType: "FILE",
Path: filePath,
ObjectId: nextID(),
},
Data: data,
}
return nil
}

func (s *FakeWorkspace) AlertsUpsert(req Request, alertId string) Response {
var alert sql.AlertV2

Expand Down Expand Up @@ -35,6 +108,13 @@ func (s *FakeWorkspace) AlertsUpsert(req Request, alertId string) Response {
alert.LifecycleState = sql.AlertLifecycleStateActive
s.Alerts[alertId] = alert

if err := s.writeAlertFile(alert); err != nil {
return Response{
Body: fmt.Sprintf("internal error: %s", err),
StatusCode: http.StatusInternalServerError,
}
}

return Response{
StatusCode: 200,
Body: alert,
Expand All @@ -51,6 +131,8 @@ func (s *FakeWorkspace) AlertsDelete(alertId string, purge bool) Response {
}
}

delete(s.files, alertFilePath(alert))

if purge {
delete(s.Alerts, alertId)
} else {
Expand Down
15 changes: 14 additions & 1 deletion libs/testserver/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,20 @@ func AddDefaultHandlers(server *Server) {

server.Handle("GET", "/api/2.0/workspace/export", func(req Request) any {
path := req.URL.Query().Get("path")
return req.Workspace.WorkspaceExport(path)
data := req.Workspace.WorkspaceExport(path)

// The filer reads the raw object body via ?direct_download=true, while
// the SDK's Workspace.Export (used by `databricks workspace export`)
// requests JSON and expects the base64-encoded content field.
if req.URL.Query().Get("direct_download") == "true" {
return data
}

return Response{
Body: workspace.ExportResponse{
Content: base64.StdEncoding.EncodeToString(data),
},
}
})

server.Handle("POST", "/api/2.0/workspace/delete", func(req Request) any {
Expand Down
Loading