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 }