mirror of https://github.com/databricks/cli.git
219 lines
6.1 KiB
Go
219 lines
6.1 KiB
Go
package run
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"net/url"
|
|
|
|
"github.com/databricks/bricks/bundle"
|
|
"github.com/databricks/bricks/bundle/config/resources"
|
|
"github.com/databricks/databricks-sdk-go/client"
|
|
"github.com/databricks/databricks-sdk-go/service/pipelines"
|
|
"github.com/fatih/color"
|
|
flag "github.com/spf13/pflag"
|
|
)
|
|
|
|
// TODO: Use a sdk implementation of this API once it's incorporated in the openapi
|
|
// spec. https://databricks.atlassian.net/browse/DECO-573
|
|
type pipelineEventErrorException struct {
|
|
ClassName string `json:"class_name"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type pipelineEventError struct {
|
|
Exceptions []pipelineEventErrorException `json:"exceptions"`
|
|
}
|
|
|
|
type pipelineEventOrigin struct {
|
|
UpdateId string `json:"update_id"`
|
|
}
|
|
|
|
type pipelineEvent struct {
|
|
Error *pipelineEventError `json:"error"`
|
|
Message string `json:"message"`
|
|
Origin pipelineEventOrigin `json:"origin"`
|
|
}
|
|
|
|
type pipelineEventsResponse struct {
|
|
Events []pipelineEvent `json:"events"`
|
|
}
|
|
|
|
func (r *pipelineRunner) logErrorEvent(ctx context.Context, pipelineId string, updateId string) error {
|
|
apiClient, err := client.New(r.bundle.WorkspaceClient().Config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
filter := url.QueryEscape(`level='ERROR'`)
|
|
apiPath := fmt.Sprintf("/api/2.0/pipelines/%s/events?filter=%s&max_results=100", pipelineId, filter)
|
|
res := pipelineEventsResponse{}
|
|
err = apiClient.Do(ctx, http.MethodGet, apiPath, nil, &res)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(res.Events) == 0 {
|
|
return nil
|
|
}
|
|
var latestEvent *pipelineEvent
|
|
// Note: For a 100 percent correct solution we should use the pagination token to find
|
|
// a last event which took place for updateId incase it's not present in the first 100 events.
|
|
// However the changes of the error event not being present in the last 100 events
|
|
// for the pipeline are pretty much 0, and this would not be worth the additional complexity
|
|
// and latency cost for that extremely rare edge case
|
|
for i := 0; i < len(res.Events); i++ {
|
|
if res.Events[i].Origin.UpdateId == updateId {
|
|
latestEvent = &res.Events[i]
|
|
break
|
|
}
|
|
}
|
|
if latestEvent == nil {
|
|
return nil
|
|
}
|
|
red := color.New(color.FgRed).SprintFunc()
|
|
errorPrefix := fmt.Sprintf("%s [%s]", red("[ERROR]"), r.Key())
|
|
logString := errorPrefix
|
|
if latestEvent.Message != "" {
|
|
logString += fmt.Sprintf(" %s\n", latestEvent.Message)
|
|
}
|
|
if latestEvent.Error != nil && len(latestEvent.Error.Exceptions) > 0 {
|
|
logString += "trace for most recent exception: \n"
|
|
for i := 0; i < len(latestEvent.Error.Exceptions); i++ {
|
|
logString += fmt.Sprintf("%s\n", latestEvent.Error.Exceptions[i].Message)
|
|
}
|
|
}
|
|
if logString != errorPrefix {
|
|
log.Print(logString)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PipelineOptions defines options for running a pipeline update.
|
|
type PipelineOptions struct {
|
|
// Perform a full graph update.
|
|
RefreshAll bool
|
|
|
|
// List of tables to update.
|
|
Refresh []string
|
|
|
|
// Perform a full graph reset and recompute.
|
|
FullRefreshAll bool
|
|
|
|
// List of tables to reset and recompute.
|
|
FullRefresh []string
|
|
}
|
|
|
|
func (o *PipelineOptions) Define(fs *flag.FlagSet) {
|
|
fs.BoolVar(&o.RefreshAll, "refresh-all", false, "Perform a full graph update.")
|
|
fs.StringSliceVar(&o.Refresh, "refresh", nil, "List of tables to update.")
|
|
fs.BoolVar(&o.FullRefreshAll, "full-refresh-all", false, "Perform a full graph reset and recompute.")
|
|
fs.StringSliceVar(&o.FullRefresh, "full-refresh", nil, "List of tables to reset and recompute.")
|
|
}
|
|
|
|
// Validate returns if the combination of options is valid.
|
|
func (o *PipelineOptions) Validate() error {
|
|
set := []string{}
|
|
if o.RefreshAll {
|
|
set = append(set, "--refresh-all")
|
|
}
|
|
if len(o.Refresh) > 0 {
|
|
set = append(set, "--refresh")
|
|
}
|
|
if o.FullRefreshAll {
|
|
set = append(set, "--full-refresh-all")
|
|
}
|
|
if len(o.FullRefresh) > 0 {
|
|
set = append(set, "--full-refresh")
|
|
}
|
|
if len(set) > 1 {
|
|
return fmt.Errorf("pipeline run arguments are mutually exclusive (got %s)", strings.Join(set, ", "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (o *PipelineOptions) toPayload(pipelineID string) (*pipelines.StartUpdate, error) {
|
|
if err := o.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
payload := &pipelines.StartUpdate{
|
|
PipelineId: pipelineID,
|
|
|
|
// Note: `RefreshAll` is implied if the fields below are not set.
|
|
RefreshSelection: o.Refresh,
|
|
FullRefresh: o.FullRefreshAll,
|
|
FullRefreshSelection: o.FullRefresh,
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
type pipelineRunner struct {
|
|
key
|
|
|
|
bundle *bundle.Bundle
|
|
pipeline *resources.Pipeline
|
|
}
|
|
|
|
func (r *pipelineRunner) Run(ctx context.Context, opts *Options) error {
|
|
var prefix = fmt.Sprintf("[INFO] [%s]", r.Key())
|
|
var pipelineID = r.pipeline.ID
|
|
|
|
w := r.bundle.WorkspaceClient()
|
|
_, err := w.Pipelines.GetByPipelineId(ctx, pipelineID)
|
|
if err != nil {
|
|
log.Printf("[WARN] Cannot get pipeline: %s", err)
|
|
return err
|
|
}
|
|
|
|
req, err := opts.Pipeline.toPayload(pipelineID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
res, err := w.Pipelines.StartUpdate(ctx, *req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
updateID := res.UpdateId
|
|
|
|
// Log the pipeline update URL as soon as it is available.
|
|
updateUrl := fmt.Sprintf("%s/#joblist/pipelines/%s/updates/%s", w.Config.Host, pipelineID, updateID)
|
|
log.Printf("%s Update available at %s", prefix, updateUrl)
|
|
|
|
// Poll update for completion and post status.
|
|
// Note: there is no "StartUpdateAndWait" wrapper for this API.
|
|
var prevState *pipelines.UpdateInfoState
|
|
for {
|
|
update, err := w.Pipelines.GetUpdateByPipelineIdAndUpdateId(ctx, pipelineID, updateID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Log only if the current state is different from the previous state.
|
|
state := update.Update.State
|
|
if prevState == nil || *prevState != state {
|
|
log.Printf("%s Update status: %s", prefix, state)
|
|
prevState = &state
|
|
}
|
|
|
|
if state == pipelines.UpdateInfoStateCanceled {
|
|
log.Printf("%s Update was cancelled!", prefix)
|
|
return fmt.Errorf("update cancelled")
|
|
}
|
|
if state == pipelines.UpdateInfoStateFailed {
|
|
log.Printf("%s Update has failed!", prefix)
|
|
r.logErrorEvent(ctx, pipelineID, updateID)
|
|
return fmt.Errorf("update failed")
|
|
}
|
|
if state == pipelines.UpdateInfoStateCompleted {
|
|
log.Printf("%s Update has completed successfully!", prefix)
|
|
return nil
|
|
}
|
|
|
|
time.Sleep(time.Second)
|
|
}
|
|
}
|