diff --git a/internal/commands/root.go b/internal/commands/root.go index 3cda458..0e329da 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -39,16 +39,13 @@ var rootCmd = &cobra.Command{ Version: Version(), } -var headless bool -var remote bool - func init() { // Add a 'version' command, in addition to the '--version' option that is auto created rootCmd.AddCommand(newVersionCmd()) // add the remote & headless options in case people want to run a split setup - rootCmd.Flags().BoolVar(&headless, "headless", false, "start the system in headless webserver only, no ui.") - rootCmd.Flags().BoolVar(&remote, "remote", false, "start the system in remote ui only, no webserver.") + rootCmd.Flags().BoolVar(&configs.Headless, "headless", false, "start the system in headless webserver only, no ui.") + rootCmd.Flags().BoolVar(&configs.Remote, "remote", false, "start the system in remote ui only, no webserver.") } func launch(cmd *cobra.Command, args []string) { @@ -87,6 +84,7 @@ func launch(cmd *cobra.Command, args []string) { } // just a little ditty to see if we should open the ui or the webservice or both + // this is done as a switch insted of an if else because our linter prefers switches to if elses switch handleStartType() { case "headless": // start webservice and listen for the the ctl + c to exit @@ -122,13 +120,13 @@ func startElectron() { // TODO: determine if cobra can make flags exclusive without the extra logic func handleStartType() string { st := "default" - if remote && headless { + if configs.Remote && configs.Headless { log.Fatalf("Cannot set both --remote and --headless flags") } - if remote { + if configs.Remote { st = "remote" - } else if headless { + } else if configs.Headless { st = "headless" } diff --git a/internal/configs/configs.go b/internal/configs/configs.go index 140d86a..d6750b2 100755 --- a/internal/configs/configs.go +++ b/internal/configs/configs.go @@ -25,6 +25,9 @@ import ( // variables related to UI config var ( UIConfig Config + + Headless bool + Remote bool ) // Config basic structure to hold configuration params for Airship UI @@ -98,7 +101,7 @@ type WsSubComponentType string // constants related to specific request/component/subcomponent types for WsRequests const ( AirshipCTL WsRequestType = "airshipctl" - Electron WsRequestType = "electron" + AirshipUI WsRequestType = "airshipui" Alert WsRequestType = "alert" Authcomplete WsComponentType = "authcomplete" diff --git a/internal/integrations/ctl/airshipctl.go b/internal/integrations/ctl/airshipctl.go index e3bc18d..2fe46c6 100755 --- a/internal/integrations/ctl/airshipctl.go +++ b/internal/integrations/ctl/airshipctl.go @@ -32,6 +32,13 @@ var ( basepath = filepath.Dir(b) ) +// CTLFunctionMap is a function map for the CTL functions that is referenced in the webservice +var CTLFunctionMap = map[configs.WsComponentType]func(configs.WsMessage) configs.WsMessage{ + configs.CTLConfig: HandleConfigRequest, + configs.Baremetal: HandleBaremetalRequest, + configs.Document: HandleDocumentRequest, +} + // maintain the state of a potentially long running process var runningRequests map[configs.WsSubComponentType]bool = make(map[configs.WsSubComponentType]bool) diff --git a/internal/webservice/server.go b/internal/webservice/server.go index cec3837..759a27f 100755 --- a/internal/webservice/server.go +++ b/internal/webservice/server.go @@ -15,147 +15,34 @@ package webservice import ( - "fmt" "log" "net/http" + "os" + "path/filepath" "time" - "github.com/gorilla/websocket" "opendev.org/airship/airshipui/internal/configs" "opendev.org/airship/airshipui/internal/integrations/ctl" ) -// 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 // the message of a specifc component is shunted to that subsystem for further processing -// TODO: make this a dynamic registration of components var functionMap = map[configs.WsRequestType]map[configs.WsComponentType]func(configs.WsMessage) configs.WsMessage{ - configs.Electron: { + configs.AirshipUI: { configs.Keepalive: keepaliveReply, configs.Initialize: clientInit, }, - configs.AirshipCTL: { - configs.CTLConfig: ctl.HandleConfigRequest, - configs.Baremetal: ctl.HandleBaremetalRequest, - configs.Document: ctl.HandleDocumentRequest, - }, + configs.AirshipCTL: ctl.CTLFunctionMap, } -// 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] = configs.WsMessage{} - Alerts = Alerts[1:] - } - - go onMessage() -} - -// handle messaging to the client -func onMessage() { - // just in case clean up the websocket - defer onClose() - - for { - var request configs.WsMessage - 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 { - // get the response and tag the timestamp so it's not repeated across all functions - response := component(request) - response.Timestamp = time.Now().UnixNano() / 1000000 - if err = ws.WriteJSON(response); err != nil { - onError(err) - break - } - } else { - if err = ws.WriteJSON(requestErrorHelper(fmt.Sprintf("Requested component: %s, not found", - request.Component), request)); err != nil { - onError(err) - break - } - log.Printf("Requested component: %s, not found\n", request.Component) - } - } else { - if err = ws.WriteJSON(requestErrorHelper(fmt.Sprintf("Requested type: %s, not found", - request.Type), request)); err != nil { - onError(err) - break - } - log.Printf("Requested type: %s, not found\n", request.Type) - } - } -} - -func requestErrorHelper(err string, request configs.WsMessage) configs.WsMessage { - return configs.WsMessage{ - Type: request.Type, - Component: request.Component, - Timestamp: time.Now().UnixNano() / 1000000, - Error: err, - } -} - -// 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(configs.WsMessage) configs.WsMessage { - return configs.WsMessage{ - Type: configs.Electron, - Component: configs.Keepalive, - } -} - -// 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) { +func handleAuth(response http.ResponseWriter, request *http.Request) { // TODO: handle the response body to capture the credentials err := ws.WriteJSON(configs.WsMessage{ - Type: configs.Electron, + Type: configs.AirshipUI, Component: configs.Authcomplete, Timestamp: time.Now().UnixNano() / 1000000, }) @@ -169,34 +56,34 @@ func handleAuth(w http.ResponseWriter, r *http.Request) { } // WebServer will run the handler functions for WebSockets -// TODO: potentially add in the ability to serve static content func WebServer() { + webServerMux := http.NewServeMux() + // some things may need a redirect so we'll give them a url to do that with - http.HandleFunc("/auth", handleAuth) + webServerMux.HandleFunc("/auth", handleAuth) // hand off the websocket upgrade over http - http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { - onOpen(w, r) + webServerMux.HandleFunc("/ws", func(response http.ResponseWriter, request *http.Request) { + onOpen(response, request) }) + // We can serve up static content if it's flagged as headless on command line + if configs.Headless { + // static file server + path, err := os.Getwd() + if err != nil { + log.Println(err) + } + + staticContent := filepath.Join(path + string(os.PathSeparator) + "web") + log.Println("Attempting to serve static content from ", staticContent) + fs := http.FileServer(http.Dir(staticContent)) + webServerMux.Handle("/", fs) + } + + // TODO: pull ports out into conf files log.Println("Attempting to start webservice on localhost:8080") - if err := http.ListenAndServe(":8080", nil); err != nil { + if err := http.ListenAndServe(":8080", webServerMux); err != nil { log.Fatal("ListenAndServe:", err) } } - -func clientInit(configs.WsMessage) configs.WsMessage { - // if no auth method is supplied start with minimal functionality - if configs.UIConfig.AuthMethod == nil { - isAuthenticated = true - } - - return configs.WsMessage{ - Type: configs.Electron, - Component: configs.Initialize, - IsAuthenticated: isAuthenticated, - Dashboards: configs.UIConfig.Clusters, - Plugins: configs.UIConfig.Plugins, - Authentication: configs.UIConfig.AuthMethod, - } -} diff --git a/internal/webservice/server_test.go b/internal/webservice/server_test.go index 801b8d0..e8e7eb3 100644 --- a/internal/webservice/server_test.go +++ b/internal/webservice/server_test.go @@ -15,15 +15,12 @@ package webservice import ( - "encoding/json" "net/http" "net/url" "testing" "time" "opendev.org/airship/airshipui/internal/configs" - "opendev.org/airship/airshipui/internal/integrations/ctl" - "opendev.org/airship/airshipui/testutil" "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" @@ -34,10 +31,10 @@ const ( serverAddr string = "localhost:8080" // client messages - initialize string = `{"type":"electron","component":"initialize"}` - keepalive string = `{"type":"electron","component":"keepalive"}` + initialize string = `{"type":"airshipui","component":"initialize"}` + keepalive string = `{"type":"airshipui","component":"keepalive"}` unknownType string = `{"type":"fake_type","component":"initialize"}` - unknownComponent string = `{"type":"electron","component":"fake_component"}` + unknownComponent string = `{"type":"airshipui","component":"fake_component"}` document string = `{"type":"airshipctl","component":"document","subcomponent":"getDefaults"}` baremetal string = `{"type":"airshipctl","component":"baremetal","subcomponent":"getDefaults"}` config string = `{"type":"airshipctl","component":"config","subcomponent":"getDefaults"}` @@ -47,126 +44,6 @@ func init() { go WebServer() } -func TestClientInit(t *testing.T) { - client, err := NewTestClient() - require.NoError(t, err) - defer client.Close() - - // simulate config provided by airshipui.json - configs.UIConfig = testutil.DummyCompleteConfig() - - // get server response to "initialize" message from client - response, err := getResponse(client, initialize) - require.NoError(t, err) - - expected := configs.WsMessage{ - Type: configs.Electron, - Component: configs.Initialize, - IsAuthenticated: false, - Dashboards: []configs.Cluster{ - testutil.DummyClusterConfig(), - }, - Plugins: []configs.Plugin{ - testutil.DummyPluginWithDashboardConfig(), - testutil.DummyPluginNoDashboard(), - }, - Authentication: testutil.DummyAuthMethodConfig(), - // don't fail on timestamp diff - Timestamp: response.Timestamp, - } - - assert.Equal(t, expected, response) -} - -func TestClientInitNoAuth(t *testing.T) { - client, err := NewTestClient() - require.NoError(t, err) - defer client.Close() - - // simulate config provided by airshipui.json - configs.UIConfig = testutil.DummyConfigNoAuth() - - isAuthenticated = false - - response, err := getResponse(client, initialize) - require.NoError(t, err) - - expected := configs.WsMessage{ - Type: configs.Electron, - Component: configs.Initialize, - // isAuthenticated should now be true in response - IsAuthenticated: true, - Dashboards: []configs.Cluster{ - testutil.DummyClusterConfig(), - }, - Plugins: []configs.Plugin{ - testutil.DummyPluginWithDashboardConfig(), - testutil.DummyPluginNoDashboard(), - }, - // don't fail on timestamp diff - Timestamp: response.Timestamp, - } - - assert.Equal(t, expected, response) -} - -func TestKeepalive(t *testing.T) { - client, err := NewTestClient() - require.NoError(t, err) - defer client.Close() - - // get server response to "keepalive" message from client - response, err := getResponse(client, keepalive) - require.NoError(t, err) - - expected := configs.WsMessage{ - Type: configs.Electron, - Component: configs.Keepalive, - // don't fail on timestamp diff - Timestamp: response.Timestamp, - } - - assert.Equal(t, expected, response) -} - -func TestUnknownType(t *testing.T) { - client, err := NewTestClient() - require.NoError(t, err) - defer client.Close() - - response, err := getResponse(client, unknownType) - require.NoError(t, err) - - expected := configs.WsMessage{ - Type: "fake_type", - Component: configs.Initialize, - // don't fail on timestamp diff - Timestamp: response.Timestamp, - Error: "Requested type: fake_type, not found", - } - - assert.Equal(t, expected, response) -} - -func TestUnknownComponent(t *testing.T) { - client, err := NewTestClient() - require.NoError(t, err) - defer client.Close() - - response, err := getResponse(client, unknownComponent) - require.NoError(t, err) - - expected := configs.WsMessage{ - Type: configs.Electron, - Component: "fake_component", - // don't fail on timestamp diff - Timestamp: response.Timestamp, - Error: "Requested component: fake_component, not found", - } - - assert.Equal(t, expected, response) -} - func TestHandleAuth(t *testing.T) { client, err := NewTestClient() require.NoError(t, err) @@ -183,7 +60,7 @@ func TestHandleAuth(t *testing.T) { require.NoError(t, err) expected := configs.WsMessage{ - Type: configs.Electron, + Type: configs.AirshipUI, Component: configs.Authcomplete, // don't fail on timestamp diff Timestamp: response.Timestamp, @@ -194,101 +71,6 @@ func TestHandleAuth(t *testing.T) { assert.Equal(t, expected, response) } -func TestHandleDocumentRequest(t *testing.T) { - client, err := NewTestClient() - require.NoError(t, err) - defer client.Close() - - expectedHTML, err := ctl.GetDocumentHTML() - require.NoError(t, err) - - response, err := getResponse(client, document) - require.NoError(t, err) - - expected := configs.WsMessage{ - Type: configs.AirshipCTL, - Component: configs.Document, - SubComponent: configs.GetDefaults, - HTML: expectedHTML, - // don't fail on timestamp diff - Timestamp: response.Timestamp, - } - - // the non typed interface requires us to break up the checking otherwise the 2 will never be equal - assert.Equal(t, expected.Type, response.Type) - assert.Equal(t, expected.Component, response.Component) - assert.Equal(t, expected.SubComponent, response.SubComponent) - assert.Equal(t, expected.HTML, response.HTML) -} - -func TestHandleBaremetalRequest(t *testing.T) { - client, err := NewTestClient() - require.NoError(t, err) - defer client.Close() - - expectedHTML, err := ctl.GetBaremetalHTML() - require.NoError(t, err) - - response, err := getResponse(client, baremetal) - require.NoError(t, err) - - expected := configs.WsMessage{ - Type: configs.AirshipCTL, - Component: configs.Baremetal, - SubComponent: configs.GetDefaults, - HTML: expectedHTML, - // don't fail on timestamp diff - Timestamp: response.Timestamp, - } - - assert.Equal(t, expected, response) -} - -func TestHandleConfigRequest(t *testing.T) { - client, err := NewTestClient() - require.NoError(t, err) - defer client.Close() - - response, err := getResponse(client, config) - require.NoError(t, err) - - expected := configs.WsMessage{ - Type: configs.AirshipCTL, - Component: configs.CTLConfig, - SubComponent: configs.GetDefaults, - } - - assert.Equal(t, expected.Type, response.Type) - assert.Equal(t, expected.Component, response.Component) - assert.Equal(t, expected.SubComponent, response.SubComponent) - - // NOTE(mfuller): integrations/ctl 'client' gets initialized - // *before* any env vars can be set here in tests, so client - // will always be initialized with default config file locations. - // Client is not exported, so we can't set it directly here. We'll - // simply make sure there's no Error value and that HTML has - // len > 0. Full testing of this response is covered in the - // integrations/ctl tests. - - assert.Len(t, response.Error, 0) - assert.Greater(t, len(response.HTML), 0) -} - -func getResponse(client *websocket.Conn, message string) (configs.WsMessage, error) { - err := client.WriteJSON(json.RawMessage(message)) - if err != nil { - return configs.WsMessage{}, err - } - - var response configs.WsMessage - err = client.ReadJSON(&response) - if err != nil { - return configs.WsMessage{}, err - } - - return response, nil -} - func NewTestClient() (*websocket.Conn, error) { var err error var client *websocket.Conn diff --git a/internal/webservice/websocket.go b/internal/webservice/websocket.go new file mode 100755 index 0000000..3dea286 --- /dev/null +++ b/internal/webservice/websocket.go @@ -0,0 +1,151 @@ +/* + 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" + + "github.com/gorilla/websocket" + "opendev.org/airship/airshipui/internal/configs" +) + +// gorilla ws specific HTTP upgrade to WebSockets +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + +// websocket that'll be reused by several places +var ws *websocket.Conn + +// handle the origin request & upgrade to websocket +func onOpen(response http.ResponseWriter, request *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(response, request, nil) + if err != nil { + log.Printf("Could not open websocket connection from: %s\n", request.Host) + http.Error(response, "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] = configs.WsMessage{} + Alerts = Alerts[1:] + } + + go onMessage() +} + +// handle messaging to the client +func onMessage() { + // just in case clean up the websocket + defer onClose() + + for { + var request configs.WsMessage + 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 { + // get the response and tag the timestamp so it's not repeated across all functions + response := component(request) + response.Timestamp = time.Now().UnixNano() / 1000000 + if err = ws.WriteJSON(response); err != nil { + onError(err) + break + } + } else { + if err = ws.WriteJSON(requestErrorHelper(fmt.Sprintf("Requested component: %s, not found", + request.Component), request)); err != nil { + onError(err) + break + } + log.Printf("Requested component: %s, not found\n", request.Component) + } + } else { + if err = ws.WriteJSON(requestErrorHelper(fmt.Sprintf("Requested type: %s, not found", + request.Type), request)); err != nil { + onError(err) + break + } + log.Printf("Requested type: %s, not found\n", request.Type) + } + } +} + +// 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) +} + +// The keepalive response including a timestamp from the server +// The UI will occasionally ping the server due to the websocket default timeout +func keepaliveReply(configs.WsMessage) configs.WsMessage { + return configs.WsMessage{ + Type: configs.AirshipUI, + Component: configs.Keepalive, + } +} + +// formats an error response in the way that we're expecting on the UI +func requestErrorHelper(err string, request configs.WsMessage) configs.WsMessage { + return configs.WsMessage{ + Type: request.Type, + Component: request.Component, + Timestamp: time.Now().UnixNano() / 1000000, + Error: err, + } +} + +// this is generated on the onOpen event and sends the information the UI needs to startup +func clientInit(configs.WsMessage) configs.WsMessage { + // if no auth method is supplied start with minimal functionality + if configs.UIConfig.AuthMethod == nil { + isAuthenticated = true + } + + return configs.WsMessage{ + Type: configs.AirshipUI, + Component: configs.Initialize, + IsAuthenticated: isAuthenticated, + Dashboards: configs.UIConfig.Clusters, + Plugins: configs.UIConfig.Plugins, + Authentication: configs.UIConfig.AuthMethod, + } +} diff --git a/internal/webservice/websocket_test.go b/internal/webservice/websocket_test.go new file mode 100755 index 0000000..c95b499 --- /dev/null +++ b/internal/webservice/websocket_test.go @@ -0,0 +1,242 @@ +/* + 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 ( + "encoding/json" + "testing" + + "github.com/gorilla/websocket" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "opendev.org/airship/airshipui/internal/configs" + "opendev.org/airship/airshipui/internal/integrations/ctl" + "opendev.org/airship/airshipui/testutil" +) + +func TestClientInit(t *testing.T) { + client, err := NewTestClient() + require.NoError(t, err) + defer client.Close() + + // simulate config provided by airshipui.json + configs.UIConfig = testutil.DummyCompleteConfig() + + // get server response to "initialize" message from client + response, err := getResponse(client, initialize) + require.NoError(t, err) + + expected := configs.WsMessage{ + Type: configs.AirshipUI, + Component: configs.Initialize, + IsAuthenticated: true, + Dashboards: []configs.Cluster{ + testutil.DummyClusterConfig(), + }, + Plugins: []configs.Plugin{ + testutil.DummyPluginWithDashboardConfig(), + testutil.DummyPluginNoDashboard(), + }, + Authentication: testutil.DummyAuthMethodConfig(), + // don't fail on timestamp diff + Timestamp: response.Timestamp, + } + + assert.Equal(t, expected, response) +} + +func TestClientInitNoAuth(t *testing.T) { + client, err := NewTestClient() + require.NoError(t, err) + defer client.Close() + + // simulate config provided by airshipui.json + configs.UIConfig = testutil.DummyConfigNoAuth() + + isAuthenticated = false + + response, err := getResponse(client, initialize) + require.NoError(t, err) + + expected := configs.WsMessage{ + Type: configs.AirshipUI, + Component: configs.Initialize, + // isAuthenticated should now be true in response + IsAuthenticated: true, + Dashboards: []configs.Cluster{ + testutil.DummyClusterConfig(), + }, + Plugins: []configs.Plugin{ + testutil.DummyPluginWithDashboardConfig(), + testutil.DummyPluginNoDashboard(), + }, + // don't fail on timestamp diff + Timestamp: response.Timestamp, + } + + assert.Equal(t, expected, response) +} + +func TestKeepalive(t *testing.T) { + client, err := NewTestClient() + require.NoError(t, err) + defer client.Close() + + // get server response to "keepalive" message from client + response, err := getResponse(client, keepalive) + require.NoError(t, err) + + expected := configs.WsMessage{ + Type: configs.AirshipUI, + Component: configs.Keepalive, + // don't fail on timestamp diff + Timestamp: response.Timestamp, + } + + assert.Equal(t, expected, response) +} + +func TestUnknownType(t *testing.T) { + client, err := NewTestClient() + require.NoError(t, err) + defer client.Close() + + response, err := getResponse(client, unknownType) + require.NoError(t, err) + + expected := configs.WsMessage{ + Type: "fake_type", + Component: configs.Initialize, + // don't fail on timestamp diff + Timestamp: response.Timestamp, + Error: "Requested type: fake_type, not found", + } + + assert.Equal(t, expected, response) +} + +func TestUnknownComponent(t *testing.T) { + client, err := NewTestClient() + require.NoError(t, err) + defer client.Close() + + response, err := getResponse(client, unknownComponent) + require.NoError(t, err) + + expected := configs.WsMessage{ + Type: configs.AirshipUI, + Component: "fake_component", + // don't fail on timestamp diff + Timestamp: response.Timestamp, + Error: "Requested component: fake_component, not found", + } + + assert.Equal(t, expected, response) +} + +func TestHandleDocumentRequest(t *testing.T) { + client, err := NewTestClient() + require.NoError(t, err) + defer client.Close() + + expectedHTML, err := ctl.GetDocumentHTML() + require.NoError(t, err) + + response, err := getResponse(client, document) + require.NoError(t, err) + + expected := configs.WsMessage{ + Type: configs.AirshipCTL, + Component: configs.Document, + SubComponent: configs.GetDefaults, + HTML: expectedHTML, + // don't fail on timestamp diff + Timestamp: response.Timestamp, + } + + // the non typed interface requires us to break up the checking otherwise the 2 will never be equal + assert.Equal(t, expected.Type, response.Type) + assert.Equal(t, expected.Component, response.Component) + assert.Equal(t, expected.SubComponent, response.SubComponent) + assert.Equal(t, expected.HTML, response.HTML) +} + +func TestHandleBaremetalRequest(t *testing.T) { + client, err := NewTestClient() + require.NoError(t, err) + defer client.Close() + + expectedHTML, err := ctl.GetBaremetalHTML() + require.NoError(t, err) + + response, err := getResponse(client, baremetal) + require.NoError(t, err) + + expected := configs.WsMessage{ + Type: configs.AirshipCTL, + Component: configs.Baremetal, + SubComponent: configs.GetDefaults, + HTML: expectedHTML, + // don't fail on timestamp diff + Timestamp: response.Timestamp, + } + + assert.Equal(t, expected, response) +} + +func TestHandleConfigRequest(t *testing.T) { + client, err := NewTestClient() + require.NoError(t, err) + defer client.Close() + + response, err := getResponse(client, config) + require.NoError(t, err) + + expected := configs.WsMessage{ + Type: configs.AirshipCTL, + Component: configs.CTLConfig, + SubComponent: configs.GetDefaults, + } + + assert.Equal(t, expected.Type, response.Type) + assert.Equal(t, expected.Component, response.Component) + assert.Equal(t, expected.SubComponent, response.SubComponent) + + // NOTE(mfuller): integrations/ctl 'client' gets initialized + // *before* any env vars can be set here in tests, so client + // will always be initialized with default config file locations. + // Client is not exported, so we can't set it directly here. We'll + // simply make sure there's no Error value and that HTML has + // len > 0. Full testing of this response is covered in the + // integrations/ctl tests. + + assert.Len(t, response.Error, 0) + assert.Greater(t, len(response.HTML), 0) +} + +func getResponse(client *websocket.Conn, message string) (configs.WsMessage, error) { + err := client.WriteJSON(json.RawMessage(message)) + if err != nil { + return configs.WsMessage{}, err + } + + var response configs.WsMessage + err = client.ReadJSON(&response) + if err != nil { + return configs.WsMessage{}, err + } + + return response, nil +} diff --git a/web/js/websocket.js b/web/js/websocket.js index 9e581d1..028e37a 100755 --- a/web/js/websocket.js +++ b/web/js/websocket.js @@ -63,13 +63,13 @@ function handleMessages(message) { switch(json["type"]) { case "alert": showDismissableAlert(json["component"], json["message"], json["fade"]); break; case "airshipctl": handleCTLMessages(json); break; - case "electron": hanldleElectronMessages(json); break; + case "airshipui": hanldleAirshipUIMessages(json); break; default: console.log("Received message: " + json["type"]); break; } } -// this is a helper function for electron / base app specific messages -function hanldleElectronMessages(json) { +// this is a helper function for airshipui / base app specific messages +function hanldleAirshipUIMessages(json) { if (json["component"] === "initialize") { if (!json["isAuthenticated"]) { authenticate(json["authentication"]); @@ -99,7 +99,7 @@ function handleCTLMessages(json) { function open() { console.log("Websocket established"); - var json = { "type": "electron", "component": "initialize" }; + var json = { "type": "airshipui", "component": "initialize" }; ws.send(JSON.stringify(json)); // start up the keepalive so the websocket stays open keepAlive(); @@ -137,7 +137,7 @@ function keepAlive() { // clear the previously set timeout window.clearTimeout(timeout); window.clearInterval(timeout); - var json = { "type": "electron", "component": "keepalive" }; + var json = { "type": "airshipui", "component": "keepalive" }; ws.send(JSON.stringify(json)); timeout = window.setTimeout(keepAlive, 60000); }