Matthew Fuller 719c437398 Push errors during plugin startup to UI
Currently, errors encountered during plugin startup are logged,
but this change ensures an error message is displayed in the UI
to alert the end user about the failure.

Change-Id: I6764cca5125812070159b13ba52d9495af9a2170
2020-06-03 14:43:58 -07:00

195 lines
5.6 KiB
Go
Executable File

/*
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package webservice
import (
"fmt"
"log"
"net/http"
"time"
"opendev.org/airship/airshipui/internal/configs"
"github.com/gorilla/websocket"
)
// just a base structure to return from the web service
type wsRequest struct {
Type string `json:"type,omitempty"`
Component string `json:"component,omitempty"`
Error string `json:"error"`
Data map[string]interface{} `json:"data"`
}
// gorilla ws specific HTTP upgrade to WebSockets
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
// this is a way to allow for arbitrary messages to be processed by the backend
// most likely we will need to have sub components register with the system
// TODO: make this a dynamic registration of components
var functionMap = map[string]map[string]func() map[string]interface{}{
"electron": {
"keepalive": keepaliveReply,
"initialize": clientInit,
},
}
// websocket that'll be reused by several places
var ws *websocket.Conn
// semaphore to signal the UI to authenticate
var isAuthenticated bool
// handle the origin request & upgrade to websocket
func onOpen(w http.ResponseWriter, r *http.Request) {
// gorilla ws will give a 403 on a cross origin request, so we silence its complaints
// This happens with electron because it's sending an origin of 'file://' instead of 'localhost:8080'
upgrader.CheckOrigin = func(r *http.Request) bool { return true }
// upgrade to websocket protocol over http
log.Printf("Establishing the websocket")
wsConn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Could not open websocket connection from: %s\n", r.Host)
http.Error(w, "Could not open websocket connection", http.StatusBadRequest)
}
ws = wsConn
log.Printf("WebSocket established with %s\n", ws.RemoteAddr().String())
// send any initialization alerts to UI and clear the queue
for len(Alerts) > 0 {
sendAlertMessage(Alerts[0])
Alerts[0] = Alert{}
Alerts = Alerts[1:]
}
go onMessage()
}
// handle messaging to the client
func onMessage() {
// just in case clean up the websocket
defer onClose()
for {
var request wsRequest
err := ws.ReadJSON(&request)
if err != nil {
onError(err)
break
}
// look through the function map to find the type to handle the request
if reqType, ok := functionMap[request.Type]; ok {
// the function map may have a component (function) to process the request
if component, ok := reqType[request.Component]; ok {
if err = ws.WriteJSON(component()); err != nil {
onError(err)
break
}
} else {
request.Error = fmt.Sprintf("Requested component: %s, not found", request.Component)
if err = ws.WriteJSON(request); err != nil {
onError(err)
break
}
log.Printf("Requested component: %s, not found\n", request.Component)
}
} else {
request.Error = fmt.Sprintf("Requested type: %s, not found", request.Type)
if err = ws.WriteJSON(request); err != nil {
onError(err)
break
}
log.Printf("Requested type: %s, not found\n", request.Type)
}
}
}
// The keepalive response including a timestamp from the server
// The electron / web app will occasionally ping the server due to the websocket default timeout
func keepaliveReply() map[string]interface{} {
return map[string]interface{}{
"type": "electron",
"component": "keepalive",
"timestamp": time.Now().UnixNano() / 1000000,
}
}
// common websocket close with logging
func onClose() {
log.Printf("Closing websocket")
// ws.Close()
}
// common websocket error handling with logging
func onError(err error) {
log.Printf("Error receiving / sending message: %s\n", err)
}
// handle an auth complete attempt
func handleAuth(w http.ResponseWriter, r *http.Request) {
// TODO: handle the response body to capture the credentials
err := ws.WriteJSON(map[string]interface{}{
"type": "electron",
"component": "authcomplete",
"timestamp": time.Now().UnixNano() / 1000000,
})
// error sending the websocket request
if err != nil {
onError(err)
} else {
isAuthenticated = true
}
}
// WebServer will run the handler functions for WebSockets
// TODO: potentially add in the ability to serve static content
func WebServer() {
// some things may need a redirect so we'll give them a url to do that with
http.HandleFunc("/auth", handleAuth)
// hand off the websocket upgrade over http
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
onOpen(w, r)
})
log.Println("Attempting to start webservice on localhost:8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal("ListenAndServe:", err)
}
}
func clientInit() map[string]interface{} {
// if no auth method is supplied start with minimal functionality
if len(configs.UiConfig.AuthMethod.URL) == 0 {
isAuthenticated = true
}
return map[string]interface{}{
"type": "electron",
"component": "initialize",
"timestamp": time.Now().UnixNano() / 1000000,
"isAuthenticated": isAuthenticated,
"dashboards": configs.UiConfig.Clusters,
"plugins": configs.UiConfig.Plugins,
"authentication": configs.UiConfig.AuthMethod,
}
}