github.com/turbot/steampipe@v1.7.0-rc.0.0.20240517123944-7cef272d4458/pkg/dashboard/dashboardserver/server.go (about)

     1  package dashboardserver
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"github.com/turbot/go-kit/helpers"
     8  	typeHelpers "github.com/turbot/go-kit/types"
     9  	"github.com/turbot/steampipe/pkg/dashboard/dashboardevents"
    10  	"github.com/turbot/steampipe/pkg/dashboard/dashboardexecute"
    11  	"github.com/turbot/steampipe/pkg/db/db_common"
    12  	"github.com/turbot/steampipe/pkg/error_helpers"
    13  	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
    14  	"github.com/turbot/steampipe/pkg/workspace"
    15  	"gopkg.in/olahol/melody.v1"
    16  	"log"
    17  	"os"
    18  	"reflect"
    19  	"strings"
    20  	"sync"
    21  )
    22  
    23  type Server struct {
    24  	dbClient         db_common.Client
    25  	mutex            *sync.Mutex
    26  	dashboardClients map[string]*DashboardClientInfo
    27  	webSocket        *melody.Melody
    28  	workspace        *workspace.Workspace
    29  }
    30  
    31  func NewServer(ctx context.Context, dbClient db_common.Client, w *workspace.Workspace) (*Server, error) {
    32  	initLogSink()
    33  
    34  	OutputWait(ctx, "Starting Dashboard Server")
    35  
    36  	webSocket := melody.New()
    37  
    38  	var dashboardClients = make(map[string]*DashboardClientInfo)
    39  
    40  	var mutex = &sync.Mutex{}
    41  
    42  	server := &Server{
    43  		dbClient:         dbClient,
    44  		mutex:            mutex,
    45  		dashboardClients: dashboardClients,
    46  		webSocket:        webSocket,
    47  		workspace:        w,
    48  	}
    49  
    50  	w.RegisterDashboardEventHandler(ctx, server.HandleDashboardEvent)
    51  	err := w.SetupWatcher(ctx, dbClient, func(c context.Context, e error) {})
    52  	OutputMessage(ctx, "Workspace loaded")
    53  
    54  	return server, err
    55  }
    56  
    57  // Start starts the API server
    58  // it returns a channel which is signalled when the API server terminates
    59  func (s *Server) Start(ctx context.Context) chan struct{} {
    60  	s.initAsync(ctx)
    61  	return startAPIAsync(ctx, s.webSocket)
    62  }
    63  
    64  // Shutdown stops the API server
    65  func (s *Server) Shutdown(ctx context.Context) {
    66  	log.Println("[TRACE] Server shutdown")
    67  
    68  	if s.webSocket != nil {
    69  		log.Println("[TRACE] closing websocket")
    70  		if err := s.webSocket.Close(); err != nil {
    71  			error_helpers.ShowErrorWithMessage(ctx, err, "Websocket shutdown failed")
    72  		}
    73  		log.Println("[TRACE] closed websocket")
    74  	}
    75  
    76  	log.Println("[TRACE] Server shutdown complete")
    77  
    78  }
    79  
    80  func (s *Server) HandleDashboardEvent(ctx context.Context, event dashboardevents.DashboardEvent) {
    81  	var payloadError error
    82  	var payload []byte
    83  	defer func() {
    84  		if payloadError != nil {
    85  			// we don't expect the build functions to ever error during marshalling
    86  			// this is because the data getting marshalled are not expected to have go specific
    87  			// properties/data in them
    88  			panic(fmt.Errorf("error building payload for '%s': %v", reflect.TypeOf(event).String(), payloadError))
    89  		}
    90  	}()
    91  
    92  	switch e := event.(type) {
    93  
    94  	case *dashboardevents.WorkspaceError:
    95  		log.Printf("[TRACE] WorkspaceError event: %s", e.Error)
    96  		payload, payloadError = buildWorkspaceErrorPayload(e)
    97  		if payloadError != nil {
    98  			return
    99  		}
   100  		_ = s.webSocket.Broadcast(payload)
   101  		OutputError(ctx, e.Error)
   102  
   103  	case *dashboardevents.ExecutionStarted:
   104  		log.Printf("[TRACE] ExecutionStarted event session %s, dashboard %s", e.Session, e.Root.GetName())
   105  		payload, payloadError = buildExecutionStartedPayload(e)
   106  		if payloadError != nil {
   107  			return
   108  		}
   109  		s.writePayloadToSession(e.Session, payload)
   110  		OutputWait(ctx, fmt.Sprintf("Dashboard execution started: %s", e.Root.GetName()))
   111  
   112  	case *dashboardevents.ExecutionError:
   113  		log.Println("[TRACE] execution error event")
   114  		payload, payloadError = buildExecutionErrorPayload(e)
   115  		if payloadError != nil {
   116  			return
   117  		}
   118  
   119  		s.writePayloadToSession(e.Session, payload)
   120  		OutputError(ctx, e.Error)
   121  
   122  	case *dashboardevents.ExecutionComplete:
   123  		log.Println("[TRACE] execution complete event")
   124  		payload, payloadError = buildExecutionCompletePayload(e)
   125  		if payloadError != nil {
   126  			return
   127  		}
   128  		dashboardName := e.Root.GetName()
   129  		s.writePayloadToSession(e.Session, payload)
   130  		outputReady(ctx, fmt.Sprintf("Execution complete: %s", dashboardName))
   131  
   132  	case *dashboardevents.ControlComplete:
   133  		log.Printf("[TRACE] ControlComplete event session %s, control %s", e.Session, e.Control.GetControlId())
   134  		payload, payloadError = buildControlCompletePayload(e)
   135  		if payloadError != nil {
   136  			return
   137  		}
   138  		s.writePayloadToSession(e.Session, payload)
   139  
   140  	case *dashboardevents.ControlError:
   141  		log.Printf("[TRACE] ControlError event session %s, control %s", e.Session, e.Control.GetControlId())
   142  		payload, payloadError = buildControlErrorPayload(e)
   143  		if payloadError != nil {
   144  			return
   145  		}
   146  		s.writePayloadToSession(e.Session, payload)
   147  
   148  	case *dashboardevents.LeafNodeUpdated:
   149  		payload, payloadError = buildLeafNodeUpdatedPayload(e)
   150  		if payloadError != nil {
   151  			return
   152  		}
   153  		s.writePayloadToSession(e.Session, payload)
   154  
   155  	case *dashboardevents.DashboardChanged:
   156  		log.Println("[TRACE] DashboardChanged event")
   157  		deletedDashboards := e.DeletedDashboards
   158  		newDashboards := e.NewDashboards
   159  
   160  		changedBenchmarks := e.ChangedBenchmarks
   161  		changedCategories := e.ChangedCategories
   162  		changedContainers := e.ChangedContainers
   163  		changedControls := e.ChangedControls
   164  		changedCards := e.ChangedCards
   165  		changedCharts := e.ChangedCharts
   166  		changedDashboards := e.ChangedDashboards
   167  		changedEdges := e.ChangedEdges
   168  		changedFlows := e.ChangedFlows
   169  		changedGraphs := e.ChangedGraphs
   170  		changedHierarchies := e.ChangedHierarchies
   171  		changedImages := e.ChangedImages
   172  		changedInputs := e.ChangedInputs
   173  		changedNodes := e.ChangedNodes
   174  		changedTables := e.ChangedTables
   175  		changedTexts := e.ChangedTexts
   176  
   177  		// If nothing has changed, ignore
   178  		if len(deletedDashboards) == 0 &&
   179  			len(newDashboards) == 0 &&
   180  			len(changedBenchmarks) == 0 &&
   181  			len(changedCategories) == 0 &&
   182  			len(changedContainers) == 0 &&
   183  			len(changedControls) == 0 &&
   184  			len(changedCards) == 0 &&
   185  			len(changedCharts) == 0 &&
   186  			len(changedDashboards) == 0 &&
   187  			len(changedEdges) == 0 &&
   188  			len(changedFlows) == 0 &&
   189  			len(changedGraphs) == 0 &&
   190  			len(changedHierarchies) == 0 &&
   191  			len(changedImages) == 0 &&
   192  			len(changedInputs) == 0 &&
   193  			len(changedNodes) == 0 &&
   194  			len(changedTables) == 0 &&
   195  			len(changedTexts) == 0 {
   196  			return
   197  		}
   198  
   199  		for k, v := range s.dashboardClients {
   200  			log.Printf("[TRACE] Dashboard client: %v %v\n", k, typeHelpers.SafeString(v.Dashboard))
   201  		}
   202  
   203  		// If) any deleted/new/changed dashboards, emit an available dashboards message to clients
   204  		if len(deletedDashboards) != 0 || len(newDashboards) != 0 || len(changedDashboards) != 0 || len(changedBenchmarks) != 0 {
   205  			OutputMessage(ctx, "Available Dashboards updated")
   206  
   207  			// Emit dashboard metadata event in case there is a new mod - else the UI won't know about this mod
   208  			payload, payloadError = buildDashboardMetadataPayload(s.workspace.GetResourceMaps(), s.workspace.CloudMetadata)
   209  			if payloadError != nil {
   210  				return
   211  			}
   212  			_ = s.webSocket.Broadcast(payload)
   213  
   214  			// Emit available dashboards event
   215  			payload, payloadError = buildAvailableDashboardsPayload(s.workspace.GetResourceMaps())
   216  			if payloadError != nil {
   217  				return
   218  			}
   219  			_ = s.webSocket.Broadcast(payload)
   220  		}
   221  
   222  		var dashboardsBeingWatched []string
   223  
   224  		dashboardClients := s.getDashboardClients()
   225  		for _, dashboardClientInfo := range dashboardClients {
   226  			dashboardName := typeHelpers.SafeString(dashboardClientInfo.Dashboard)
   227  			if dashboardClientInfo.Dashboard != nil {
   228  				if helpers.StringSliceContains(dashboardsBeingWatched, dashboardName) {
   229  					continue
   230  				}
   231  				dashboardsBeingWatched = append(dashboardsBeingWatched, dashboardName)
   232  			}
   233  		}
   234  
   235  		var changedDashboardNames []string
   236  		var newDashboardNames []string
   237  
   238  		// Process the changed items and make a note of the dashboard(s) they're in
   239  		changedDashboardNames = append(changedDashboardNames, getDashboardsInterestedInResourceChanges(dashboardsBeingWatched, changedDashboardNames, changedBenchmarks)...)
   240  		changedDashboardNames = append(changedDashboardNames, getDashboardsInterestedInResourceChanges(dashboardsBeingWatched, changedDashboardNames, changedCategories)...)
   241  		changedDashboardNames = append(changedDashboardNames, getDashboardsInterestedInResourceChanges(dashboardsBeingWatched, changedDashboardNames, changedContainers)...)
   242  		changedDashboardNames = append(changedDashboardNames, getDashboardsInterestedInResourceChanges(dashboardsBeingWatched, changedDashboardNames, changedControls)...)
   243  		changedDashboardNames = append(changedDashboardNames, getDashboardsInterestedInResourceChanges(dashboardsBeingWatched, changedDashboardNames, changedCards)...)
   244  		changedDashboardNames = append(changedDashboardNames, getDashboardsInterestedInResourceChanges(dashboardsBeingWatched, changedDashboardNames, changedCharts)...)
   245  		changedDashboardNames = append(changedDashboardNames, getDashboardsInterestedInResourceChanges(dashboardsBeingWatched, changedDashboardNames, changedEdges)...)
   246  		changedDashboardNames = append(changedDashboardNames, getDashboardsInterestedInResourceChanges(dashboardsBeingWatched, changedDashboardNames, changedFlows)...)
   247  		changedDashboardNames = append(changedDashboardNames, getDashboardsInterestedInResourceChanges(dashboardsBeingWatched, changedDashboardNames, changedGraphs)...)
   248  		changedDashboardNames = append(changedDashboardNames, getDashboardsInterestedInResourceChanges(dashboardsBeingWatched, changedDashboardNames, changedHierarchies)...)
   249  		changedDashboardNames = append(changedDashboardNames, getDashboardsInterestedInResourceChanges(dashboardsBeingWatched, changedDashboardNames, changedImages)...)
   250  		changedDashboardNames = append(changedDashboardNames, getDashboardsInterestedInResourceChanges(dashboardsBeingWatched, changedDashboardNames, changedInputs)...)
   251  		changedDashboardNames = append(changedDashboardNames, getDashboardsInterestedInResourceChanges(dashboardsBeingWatched, changedDashboardNames, changedNodes)...)
   252  		changedDashboardNames = append(changedDashboardNames, getDashboardsInterestedInResourceChanges(dashboardsBeingWatched, changedDashboardNames, changedTables)...)
   253  		changedDashboardNames = append(changedDashboardNames, getDashboardsInterestedInResourceChanges(dashboardsBeingWatched, changedDashboardNames, changedTexts)...)
   254  
   255  		for _, changedDashboard := range changedDashboards {
   256  			if helpers.StringSliceContains(changedDashboardNames, changedDashboard.Name) {
   257  				continue
   258  			}
   259  			changedDashboardNames = append(changedDashboardNames, changedDashboard.Name)
   260  		}
   261  
   262  		for _, changedDashboardName := range changedDashboardNames {
   263  			sessionMap := s.getDashboardClients()
   264  			for sessionId, dashboardClientInfo := range sessionMap {
   265  				if typeHelpers.SafeString(dashboardClientInfo.Dashboard) == changedDashboardName {
   266  					_ = dashboardexecute.Executor.ExecuteDashboard(ctx, sessionId, changedDashboardName, dashboardClientInfo.DashboardInputs, s.workspace, s.dbClient)
   267  				}
   268  			}
   269  		}
   270  
   271  		// Special case - if we previously had a workspace error, any previously existing dashboards
   272  		// will come in here as new, so we need to check if any of those new dashboards are being watched.
   273  		// If so, execute them
   274  		for _, newDashboard := range newDashboards {
   275  			if helpers.StringSliceContains(newDashboardNames, newDashboard.Name()) {
   276  				continue
   277  			}
   278  			newDashboardNames = append(newDashboardNames, newDashboard.Name())
   279  		}
   280  
   281  		sessionMap := s.getDashboardClients()
   282  		for _, newDashboardName := range newDashboardNames {
   283  			for sessionId, dashboardClientInfo := range sessionMap {
   284  				if typeHelpers.SafeString(dashboardClientInfo.Dashboard) == newDashboardName {
   285  					_ = dashboardexecute.Executor.ExecuteDashboard(ctx, sessionId, newDashboardName, dashboardClientInfo.DashboardInputs, s.workspace, s.dbClient)
   286  				}
   287  			}
   288  		}
   289  
   290  	case *dashboardevents.InputValuesCleared:
   291  		log.Println("[TRACE] input values cleared event", *e)
   292  
   293  		payload, payloadError = buildInputValuesClearedPayload(e)
   294  		if payloadError != nil {
   295  			return
   296  		}
   297  
   298  		dashboardClients := s.getDashboardClients()
   299  		if sessionInfo, ok := dashboardClients[e.Session]; ok {
   300  			for _, clearedInput := range e.ClearedInputs {
   301  				delete(sessionInfo.DashboardInputs, clearedInput)
   302  			}
   303  		}
   304  		s.writePayloadToSession(e.Session, payload)
   305  	}
   306  }
   307  
   308  func (s *Server) initAsync(ctx context.Context) {
   309  	go func() {
   310  		// Return list of dashboards on connect
   311  		s.webSocket.HandleConnect(func(session *melody.Session) {
   312  			log.Println("[TRACE] client connected")
   313  			s.addSession(session)
   314  		})
   315  
   316  		s.webSocket.HandleDisconnect(func(session *melody.Session) {
   317  			log.Println("[TRACE] client disconnected")
   318  			s.clearSession(ctx, session)
   319  		})
   320  
   321  		s.webSocket.HandleMessage(s.handleMessageFunc(ctx))
   322  		OutputMessage(ctx, "Initialization complete")
   323  	}()
   324  }
   325  
   326  func (s *Server) handleMessageFunc(ctx context.Context) func(session *melody.Session, msg []byte) {
   327  	return func(session *melody.Session, msg []byte) {
   328  
   329  		sessionId := s.getSessionId(session)
   330  
   331  		var request ClientRequest
   332  		// if we could not decode message - ignore
   333  		err := json.Unmarshal(msg, &request)
   334  		if err != nil {
   335  			log.Printf("[WARN] failed to marshal message: %s", err.Error())
   336  			return
   337  		}
   338  
   339  		if request.Action != "keep_alive" {
   340  			log.Println("[TRACE] message", string(msg))
   341  		}
   342  
   343  		switch request.Action {
   344  		case "get_dashboard_metadata":
   345  			payload, err := buildDashboardMetadataPayload(s.workspace.GetResourceMaps(), s.workspace.CloudMetadata)
   346  			if err != nil {
   347  				panic(fmt.Errorf("error building payload for get_metadata: %v", err))
   348  			}
   349  			_ = session.Write(payload)
   350  		case "get_available_dashboards":
   351  			payload, err := buildAvailableDashboardsPayload(s.workspace.GetResourceMaps())
   352  			if err != nil {
   353  				panic(fmt.Errorf("error building payload for get_available_dashboards: %v", err))
   354  			}
   355  			_ = session.Write(payload)
   356  		case "select_dashboard":
   357  			s.setDashboardForSession(sessionId, request.Payload.Dashboard.FullName, request.Payload.InputValues)
   358  			_ = dashboardexecute.Executor.ExecuteDashboard(ctx, sessionId, request.Payload.Dashboard.FullName, request.Payload.InputValues, s.workspace, s.dbClient)
   359  		case "select_snapshot":
   360  			snapshotName := request.Payload.Dashboard.FullName
   361  			s.setDashboardForSession(sessionId, snapshotName, request.Payload.InputValues)
   362  			snap, err := dashboardexecute.Executor.LoadSnapshot(ctx, sessionId, snapshotName, s.workspace)
   363  			// TACTICAL- handle with error message
   364  			error_helpers.FailOnError(err)
   365  			// error handling???
   366  			payload, err := buildDisplaySnapshotPayload(snap)
   367  			// TACTICAL- handle with error message
   368  			error_helpers.FailOnError(err)
   369  
   370  			s.writePayloadToSession(sessionId, payload)
   371  			outputReady(ctx, fmt.Sprintf("Show snapshot complete: %s", snapshotName))
   372  		case "input_changed":
   373  			s.setDashboardInputsForSession(sessionId, request.Payload.InputValues)
   374  			_ = dashboardexecute.Executor.OnInputChanged(ctx, sessionId, request.Payload.InputValues, request.Payload.ChangedInput)
   375  		case "clear_dashboard":
   376  			s.setDashboardInputsForSession(sessionId, nil)
   377  			dashboardexecute.Executor.CancelExecutionForSession(ctx, sessionId)
   378  		}
   379  	}
   380  }
   381  
   382  func (s *Server) clearSession(ctx context.Context, session *melody.Session) {
   383  	if strings.ToUpper(os.Getenv("DEBUG")) == "TRUE" {
   384  		return
   385  	}
   386  
   387  	sessionId := s.getSessionId(session)
   388  
   389  	dashboardexecute.Executor.CancelExecutionForSession(ctx, sessionId)
   390  
   391  	s.deleteDashboardClient(sessionId)
   392  }
   393  
   394  func (s *Server) addSession(session *melody.Session) {
   395  	sessionId := s.getSessionId(session)
   396  
   397  	clientSession := &DashboardClientInfo{
   398  		Session: session,
   399  	}
   400  
   401  	s.addDashboardClient(sessionId, clientSession)
   402  }
   403  
   404  func (s *Server) setDashboardInputsForSession(sessionId string, inputs map[string]interface{}) {
   405  	dashboardClients := s.getDashboardClients()
   406  	if sessionInfo, ok := dashboardClients[sessionId]; ok {
   407  		sessionInfo.DashboardInputs = inputs
   408  	}
   409  }
   410  
   411  func (s *Server) getSessionId(session *melody.Session) string {
   412  	return fmt.Sprintf("%p", session)
   413  }
   414  
   415  // functions providing locked access to member properties
   416  
   417  func (s *Server) setDashboardForSession(sessionId string, dashboardName string, inputs map[string]interface{}) *DashboardClientInfo {
   418  	s.mutex.Lock()
   419  	defer s.mutex.Unlock()
   420  
   421  	dashboardClientInfo := s.dashboardClients[sessionId]
   422  	dashboardClientInfo.Dashboard = &dashboardName
   423  	dashboardClientInfo.DashboardInputs = inputs
   424  
   425  	return dashboardClientInfo
   426  }
   427  
   428  func (s *Server) writePayloadToSession(sessionId string, payload []byte) {
   429  	s.mutex.Lock()
   430  	defer s.mutex.Unlock()
   431  
   432  	if sessionInfo, ok := s.dashboardClients[sessionId]; ok {
   433  		_ = sessionInfo.Session.Write(payload)
   434  	}
   435  }
   436  
   437  func (s *Server) getDashboardClients() map[string]*DashboardClientInfo {
   438  	s.mutex.Lock()
   439  	defer s.mutex.Unlock()
   440  
   441  	return s.dashboardClients
   442  }
   443  
   444  func (s *Server) addDashboardClient(sessionId string, clientSession *DashboardClientInfo) {
   445  	s.mutex.Lock()
   446  	s.dashboardClients[sessionId] = clientSession
   447  	s.mutex.Unlock()
   448  }
   449  
   450  func (s *Server) deleteDashboardClient(sessionId string) {
   451  	s.mutex.Lock()
   452  	delete(s.dashboardClients, sessionId)
   453  	s.mutex.Unlock()
   454  }
   455  
   456  func getDashboardsInterestedInResourceChanges(dashboardsBeingWatched []string, existingChangedDashboardNames []string, changedItems []*modconfig.DashboardTreeItemDiffs) []string {
   457  	var changedDashboardNames []string
   458  
   459  	for _, changedItem := range changedItems {
   460  		paths := changedItem.Item.GetPaths()
   461  		for _, nodePath := range paths {
   462  			for _, nodeName := range nodePath {
   463  				resourceParts, _ := modconfig.ParseResourceName(nodeName)
   464  				// We only care about changes from these resource types
   465  				if !helpers.StringSliceContains([]string{modconfig.BlockTypeDashboard, modconfig.BlockTypeBenchmark}, resourceParts.ItemType) {
   466  					continue
   467  				}
   468  
   469  				if helpers.StringSliceContains(existingChangedDashboardNames, nodeName) || helpers.StringSliceContains(changedDashboardNames, nodeName) || !helpers.StringSliceContains(dashboardsBeingWatched, nodeName) {
   470  					continue
   471  				}
   472  
   473  				changedDashboardNames = append(changedDashboardNames, nodeName)
   474  			}
   475  		}
   476  	}
   477  
   478  	return changedDashboardNames
   479  }