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
16 changes: 8 additions & 8 deletions acceptance/bundle/resources/alerts/with_file/alert.dbalert.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
{
"custom_summary": "My alert from file",
"evaluation": {
"comparison_operator": "EQUAL",
"notification": {
"notify_on_ok": false,
"retrigger_seconds": 1
},
"source": {
"name": "1",
"aggregation": "MAX",
"display": "1",
"aggregation": "MAX"
"name": "1"
},
"comparison_operator": "EQUAL",
"threshold": {
"value": {
"double_value": 2.0
"double_value": 2
}
},
"notification": {
"retrigger_seconds": 1,
"notify_on_ok": false
}
},
"schedule": {
Expand Down
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/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