Merge "Restructure of the webservice components"

This commit is contained in:
Zuul 2020-07-22 15:40:26 +00:00 committed by Gerrit Code Review
commit 7b0fe7a7d1
8 changed files with 446 additions and 376 deletions

View File

@ -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"
}

View File

@ -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"

View File

@ -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)

View File

@ -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,
}
}

View File

@ -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

151
internal/webservice/websocket.go Executable file
View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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);
}