SSO ability for plugin dashboards
This facilitates the SSO solution for plugin dashboards To accomplish this we must use a reverse proxy to inject the correct header information on requests so that the backend services will accept a prelogin. This is specific to each plugin so it needs to be extensible and allow for a dashboard to customize how it's done. This also allows for a fully hosted page when running in headless mode. Fixes #42 Change-Id: I442e22a19a387ae367c2518085cdca0f2786a0a4
This commit is contained in:
parent
1d9e096fb5
commit
850df9d213
39
README.md
39
README.md
@ -25,38 +25,19 @@ $HOME/.airship/airshipui.json.
|
||||
To add service dashboards, create a section at the top level of airshipui.json as follows:
|
||||
|
||||
```
|
||||
"clusters": [
|
||||
{
|
||||
"name": "clusterA",
|
||||
"baseFqdn": "svc.cluster.local",
|
||||
"namespaces": [
|
||||
"dashboards": [
|
||||
{
|
||||
"name": "ceph",
|
||||
"dashboards": [
|
||||
{
|
||||
"name": "Ceph",
|
||||
"protocol": "https",
|
||||
"fqdn": "ceph-dash.example.domain",
|
||||
"port": 443,
|
||||
"baseURL": "https://ceph-dash.example.domain",
|
||||
"path": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "openstack",
|
||||
"dashboards": [
|
||||
{
|
||||
"name": "Horizon",
|
||||
"protocol": "http",
|
||||
"hostname": "horizon",
|
||||
"port": 80,
|
||||
"baseURL": "http://horizon",
|
||||
"path": "dashboard/auth/login"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
For dashboards that are made available through service endpoints in your cluster, the FQDN for the dashboard will be constructed using the format
|
||||
@ -68,21 +49,17 @@ For dashboards that are made available through service endpoints in your cluster
|
||||
|
||||
If both "hostname" and "fqdn" are provided, "fqdn" will take precedence.
|
||||
|
||||
The airshipui.json configuration file can also be used to launch "plugins", or external executables, in the background as Airship UI starts. Any processes
|
||||
The airshipui.json configuration file can also be used to launch external executables that server your dashboards, in the background as Airship UI starts. Any processes
|
||||
launched by Airship UI will be terminated when Airship UI exits, including any child processes started by the plugins. If the plugin launches a web
|
||||
dashboard, it can be also be included in the list of service dashboards within Airship UI. The following example demonstrates how to add configuration to
|
||||
launch and use Octant within Airship UI:
|
||||
|
||||
```
|
||||
"plugins": [
|
||||
"dashboards": [
|
||||
{
|
||||
"name": "Octant",
|
||||
"dashboard": {
|
||||
"protocol": "http",
|
||||
"fqdn": "localhost",
|
||||
"port": 7777,
|
||||
"path": ""
|
||||
},
|
||||
"baseURL": "http://localhost:7777",
|
||||
"path": "",
|
||||
"executable": {
|
||||
"autoStart": true,
|
||||
"filepath": "/usr/local/bin/octant",
|
||||
@ -96,7 +73,7 @@ launch and use Octant within Airship UI:
|
||||
]
|
||||
```
|
||||
|
||||
To prevent a plugin from launching but retain its configuration for later use, simply set "autoStart" to false.
|
||||
To prevent a dashboard executable from launching but retain its configuration for later use, simply set "autoStart" to false.
|
||||
|
||||
## Developer's Guide
|
||||
|
||||
|
1
go.sum
1
go.sum
@ -893,6 +893,7 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
k8s.io/api v0.0.0-20191016110408-35e52d86657a/go.mod h1:/L5qH+AD540e7Cetbui1tuJeXdmNhO8jM6VkXeDdDhQ=
|
||||
k8s.io/api v0.0.0-20191016225839-816a9b7df678/go.mod h1:LZQaT8MvVpl7Bg2lYFcQm7+Mpdxq8p1NFl3yh+5DCwY=
|
||||
|
@ -66,18 +66,20 @@ func launch(cmd *cobra.Command, args []string) {
|
||||
// Read AirshipUI config file
|
||||
if err := configs.SetUIConfig(airshipUIConfigPath); err == nil {
|
||||
// launch any plugins marked as autoStart: true in airshipui.json
|
||||
for _, p := range configs.UIConfig.Plugins {
|
||||
if p.Executable.AutoStart {
|
||||
for _, dashboard := range configs.UIConfig.Dashboards {
|
||||
if dashboard.Executable != nil {
|
||||
if dashboard.Executable.AutoStart {
|
||||
waitgrp.Add(1)
|
||||
go RunBinaryWithOptions(
|
||||
ctx,
|
||||
p.Executable.Filepath,
|
||||
p.Executable.Args,
|
||||
dashboard.Executable.Filepath,
|
||||
dashboard.Executable.Args,
|
||||
&waitgrp,
|
||||
sigs,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("config %s", err)
|
||||
webservice.SendAlert(configs.Info, fmt.Sprintf("%s", err), true)
|
||||
|
@ -33,8 +33,7 @@ var (
|
||||
// Config basic structure to hold configuration params for Airship UI
|
||||
type Config struct {
|
||||
AuthMethod *AuthMethod `json:"authMethod,omitempty"`
|
||||
Plugins []Plugin `json:"plugins,omitempty"`
|
||||
Clusters []Cluster `json:"clusters,omitempty"`
|
||||
Dashboards []Dashboard `json:"dashboards,omitempty"`
|
||||
}
|
||||
|
||||
// AuthMethod structure to hold authentication parameters
|
||||
@ -44,21 +43,6 @@ type AuthMethod struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// Plugin structure to hold plugin specific parameters
|
||||
type Plugin struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Dashboard *PluginDashboard `json:"dashboard,omitempty"`
|
||||
Executable *Executable `json:"executable"`
|
||||
}
|
||||
|
||||
// PluginDashboard structure to hold web dashboard parameters for plugins
|
||||
type PluginDashboard struct {
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
FQDN string `json:"fqdn,omitempty"`
|
||||
Port uint16 `json:"port,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// Executable structure to hold parameters for launching an executable plugin
|
||||
type Executable struct {
|
||||
AutoStart bool `json:"autoStart,omitempty"`
|
||||
@ -69,24 +53,10 @@ type Executable struct {
|
||||
// Dashboard structure
|
||||
type Dashboard struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
FQDN string `json:"fqdn,omitempty"`
|
||||
Port uint16 `json:"port,omitempty"`
|
||||
BaseURL string `json:"baseURL,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// Namespace structure
|
||||
type Namespace struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Dashboards []Dashboard `json:"dashboards,omitempty"`
|
||||
}
|
||||
|
||||
// Cluster basic structure describing a cluster
|
||||
type Cluster struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
BaseFqdn string `json:"baseFqdn,omitempty"`
|
||||
Namespaces []Namespace `json:"namespaces,omitempty"`
|
||||
IsProxied bool `json:"isProxied,omitempty"`
|
||||
Executable *Executable `json:"executable,omitempty"`
|
||||
}
|
||||
|
||||
// WsRequestType is used to set the specific types allowable for WsRequests
|
||||
@ -144,8 +114,7 @@ type WsMessage struct {
|
||||
YAML string `json:"yaml,omitempty"`
|
||||
|
||||
// information related to the init of the UI
|
||||
Dashboards []Cluster `json:"dashboards,omitempty"`
|
||||
Plugins []Plugin `json:"plugins,omitempty"`
|
||||
Dashboards []Dashboard `json:"dashboards,omitempty"`
|
||||
Authentication *AuthMethod `json:"authentication,omitempty"`
|
||||
AuthInfoOptions *config.AuthInfoOptions `json:"authInfoOptions,omitempty"`
|
||||
ContextOptions *config.ContextOptions `json:"contextOptions,omitempty"`
|
||||
|
@ -30,13 +30,7 @@ const (
|
||||
|
||||
func TestSetUIConfig(t *testing.T) {
|
||||
conf := configs.Config{
|
||||
Clusters: []configs.Cluster{
|
||||
testutil.DummyClusterConfig(),
|
||||
},
|
||||
Plugins: []configs.Plugin{
|
||||
testutil.DummyPluginWithDashboardConfig(),
|
||||
testutil.DummyPluginNoDashboard(),
|
||||
},
|
||||
Dashboards: testutil.DummyDashboardsConfig(),
|
||||
AuthMethod: testutil.DummyAuthMethodConfig(),
|
||||
}
|
||||
|
||||
|
33
internal/configs/testdata/airshipui.json
vendored
33
internal/configs/testdata/airshipui.json
vendored
@ -2,15 +2,11 @@
|
||||
"authMethod": {
|
||||
"url": "http://fake.auth.method.com/auth"
|
||||
},
|
||||
"plugins": [
|
||||
"dashboards": [
|
||||
{
|
||||
"name": "dummy_plugin_with_dash",
|
||||
"dashboard": {
|
||||
"protocol": "http",
|
||||
"fqdn": "localhost",
|
||||
"port": 80,
|
||||
"path": "index.html"
|
||||
},
|
||||
"name": "dummy_dashboard",
|
||||
"baseURL": "http://dummyhost",
|
||||
"path": "fake/login/path",
|
||||
"executable": {
|
||||
"autoStart": true,
|
||||
"filepath": "/fake/path/to/executable",
|
||||
@ -30,26 +26,11 @@
|
||||
"fakevalue"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"clusters": [
|
||||
},
|
||||
{
|
||||
"name": "dummy_cluster",
|
||||
"baseFqdn": "dummy.cluster.local",
|
||||
"namespaces": [
|
||||
{
|
||||
"name": "dummy_namespace",
|
||||
"dashboards": [
|
||||
{
|
||||
"name": "dummy_dashboard",
|
||||
"protocol": "http",
|
||||
"hostname": "dummyhost",
|
||||
"port": 80,
|
||||
"name": "dummy_dashboard_no_exe",
|
||||
"baseURL": "http://dummyhost",
|
||||
"path": "fake/login/path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -2,6 +2,5 @@
|
||||
"authMethod": {
|
||||
"url": "http://fake.auth.method.com/auth"
|
||||
},
|
||||
"plugins": "",
|
||||
"clusters": ""
|
||||
"dashboards": [],
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ import (
|
||||
// RunElectron executes the standalone electron app which serves up our web components
|
||||
func RunElectron() error {
|
||||
// determine ; or : depending on the OS
|
||||
sep := string(os.PathListSeparator)
|
||||
var sep = string(os.PathListSeparator)
|
||||
|
||||
// get the current working directory, should be the root of the airshipui tree
|
||||
path, err := os.Getwd()
|
||||
|
141
internal/webservice/proxy.go
Executable file
141
internal/webservice/proxy.go
Executable file
@ -0,0 +1,141 @@
|
||||
/*
|
||||
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 (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"opendev.org/airship/airshipui/internal/configs"
|
||||
)
|
||||
|
||||
// map of proxy targets which will be used based on the request
|
||||
var proxyMap = map[string]*url.URL{}
|
||||
|
||||
type transport struct {
|
||||
http.RoundTripper
|
||||
}
|
||||
|
||||
var _ http.RoundTripper = &transport{}
|
||||
|
||||
func (t *transport) RoundTrip(request *http.Request) (response *http.Response, err error) {
|
||||
// TODO: inject headers here for bearer token auth
|
||||
// example:
|
||||
// request.Header.Add("X-Auth-Token", "<token>")
|
||||
|
||||
response, err = t.RoundTripper.RoundTrip(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: inject headers here for cookie auth
|
||||
// example:
|
||||
// response.Header.Add("Set-Cookie", "sessionid=<session>; expires=<date>; HttpOnly; Max-Age=3597; Path=/;")
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// handle a proxy request
|
||||
// this is essentially a man in the middle attack that allows us to inject headers for single sign on
|
||||
func handleProxy(response http.ResponseWriter, request *http.Request) {
|
||||
// retrieve the target URL from the proxy map
|
||||
target := proxyMap[request.Host]
|
||||
|
||||
// short circuit for bad targets blowing up the backend
|
||||
if target == nil {
|
||||
response.WriteHeader(http.StatusInternalServerError)
|
||||
if _, err := response.Write([]byte("500 - Unable to locate proxy for request!")); err != nil {
|
||||
log.Println("Error writing response for proxy not found: ", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(target)
|
||||
proxy.Transport = &transport{http.DefaultTransport}
|
||||
|
||||
// Update the headers to allow for SSL redirection
|
||||
request.URL.Host = target.Host
|
||||
request.URL.Scheme = target.Scheme
|
||||
|
||||
host := request.Header.Get("Host")
|
||||
request.Header.Set("X-Forwarded-Host", host)
|
||||
request.Header.Set("X-Forwarded-For", host)
|
||||
|
||||
proxy.ServeHTTP(response, request)
|
||||
}
|
||||
|
||||
func getRandomPort() (string, error) {
|
||||
// get a random port for the proxy
|
||||
listener, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// close the port so we can start the proxy
|
||||
defer listener.Close()
|
||||
|
||||
// get the string of the port
|
||||
return listener.Addr().String(), nil
|
||||
}
|
||||
|
||||
// proxyServer will proxy dashboard connections allowing us to inject headers
|
||||
func proxyServer(port string) {
|
||||
proxyServerMux := http.NewServeMux()
|
||||
|
||||
// some things may need a helping hand with the headers so we'll proxy it for them
|
||||
proxyServerMux.HandleFunc("/", handleProxy)
|
||||
|
||||
if err := http.ListenAndServe(port, proxyServerMux); err != nil {
|
||||
log.Fatal("Error starting proxy: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
// helper function that kicks off all proxies prior to the start of the website
|
||||
func startProxies() {
|
||||
for index, dashboard := range configs.UIConfig.Dashboards {
|
||||
port, err := getRandomPort()
|
||||
if err != nil {
|
||||
log.Fatal("Error starting proxy, unable to allocate port:", err)
|
||||
}
|
||||
|
||||
// this will persuade the UI to use the proxy and not the original host
|
||||
dashboard.IsProxied = true
|
||||
|
||||
// cache up the target for the proxy url
|
||||
target, err := url.Parse(dashboard.BaseURL)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
// set the target for the proxied request to the original url
|
||||
proxyMap[port] = target
|
||||
|
||||
// set the target for the link in the ui to the proxy address
|
||||
dashboard.BaseURL = "http://" + port
|
||||
|
||||
// kick off proxy
|
||||
log.Printf("Attempting to start proxy for %s on: %s\n", dashboard.Name, port)
|
||||
|
||||
// set the dashboard from this point on to go to the proxy
|
||||
configs.UIConfig.Dashboards[index] = dashboard
|
||||
|
||||
// and away we go.........
|
||||
go proxyServer(port)
|
||||
}
|
||||
}
|
@ -22,19 +22,8 @@ import (
|
||||
"time"
|
||||
|
||||
"opendev.org/airship/airshipui/internal/configs"
|
||||
"opendev.org/airship/airshipui/internal/integrations/ctl"
|
||||
)
|
||||
|
||||
// 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
|
||||
var functionMap = map[configs.WsRequestType]map[configs.WsComponentType]func(configs.WsMessage) configs.WsMessage{
|
||||
configs.AirshipUI: {
|
||||
configs.Keepalive: keepaliveReply,
|
||||
configs.Initialize: clientInit,
|
||||
},
|
||||
configs.AirshipCTL: ctl.CTLFunctionMap,
|
||||
}
|
||||
|
||||
// semaphore to signal the UI to authenticate
|
||||
var isAuthenticated bool
|
||||
|
||||
@ -69,6 +58,9 @@ func WebServer() {
|
||||
|
||||
// We can serve up static content if it's flagged as headless on command line
|
||||
if configs.Headless {
|
||||
// start proxies for web based use
|
||||
startProxies()
|
||||
|
||||
// static file server
|
||||
path, err := os.Getwd()
|
||||
if err != nil {
|
||||
|
@ -83,5 +83,6 @@ func NewTestClient() (*websocket.Conn, error) {
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"opendev.org/airship/airshipui/internal/configs"
|
||||
"opendev.org/airship/airshipui/internal/integrations/ctl"
|
||||
)
|
||||
|
||||
// gorilla ws specific HTTP upgrade to WebSockets
|
||||
@ -33,6 +34,16 @@ var upgrader = websocket.Upgrader{
|
||||
// websocket that'll be reused by several places
|
||||
var ws *websocket.Conn
|
||||
|
||||
// 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
|
||||
var functionMap = map[configs.WsRequestType]map[configs.WsComponentType]func(configs.WsMessage) configs.WsMessage{
|
||||
configs.AirshipUI: {
|
||||
configs.Keepalive: keepaliveReply,
|
||||
configs.Initialize: clientInit,
|
||||
},
|
||||
configs.AirshipCTL: ctl.CTLFunctionMap,
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -144,8 +155,7 @@ func clientInit(configs.WsMessage) configs.WsMessage {
|
||||
Type: configs.AirshipUI,
|
||||
Component: configs.Initialize,
|
||||
IsAuthenticated: isAuthenticated,
|
||||
Dashboards: configs.UIConfig.Clusters,
|
||||
Plugins: configs.UIConfig.Plugins,
|
||||
Dashboards: configs.UIConfig.Dashboards,
|
||||
Authentication: configs.UIConfig.AuthMethod,
|
||||
}
|
||||
}
|
||||
|
@ -42,13 +42,7 @@ func TestClientInit(t *testing.T) {
|
||||
Type: configs.AirshipUI,
|
||||
Component: configs.Initialize,
|
||||
IsAuthenticated: true,
|
||||
Dashboards: []configs.Cluster{
|
||||
testutil.DummyClusterConfig(),
|
||||
},
|
||||
Plugins: []configs.Plugin{
|
||||
testutil.DummyPluginWithDashboardConfig(),
|
||||
testutil.DummyPluginNoDashboard(),
|
||||
},
|
||||
Dashboards: testutil.DummyDashboardsConfig(),
|
||||
Authentication: testutil.DummyAuthMethodConfig(),
|
||||
// don't fail on timestamp diff
|
||||
Timestamp: response.Timestamp,
|
||||
@ -75,12 +69,8 @@ func TestClientInitNoAuth(t *testing.T) {
|
||||
Component: configs.Initialize,
|
||||
// isAuthenticated should now be true in response
|
||||
IsAuthenticated: true,
|
||||
Dashboards: []configs.Cluster{
|
||||
testutil.DummyClusterConfig(),
|
||||
},
|
||||
Plugins: []configs.Plugin{
|
||||
testutil.DummyPluginWithDashboardConfig(),
|
||||
testutil.DummyPluginNoDashboard(),
|
||||
Dashboards: []configs.Dashboard{
|
||||
testutil.DummyDashboardConfig(),
|
||||
},
|
||||
// don't fail on timestamp diff
|
||||
Timestamp: response.Timestamp,
|
||||
|
@ -84,28 +84,40 @@ func InitConfig(t *testing.T) (conf *config.Config, configPath string,
|
||||
|
||||
// DummyDashboardConfig returns a populated Dashboard struct
|
||||
func DummyDashboardConfig() configs.Dashboard {
|
||||
e := DummyExecutableConfig()
|
||||
return configs.Dashboard{
|
||||
Name: "dummy_dashboard",
|
||||
Protocol: "http",
|
||||
Hostname: "dummyhost",
|
||||
Port: 80,
|
||||
BaseURL: "http://dummyhost",
|
||||
Path: "fake/login/path",
|
||||
Executable: e,
|
||||
}
|
||||
}
|
||||
|
||||
// DummyPluginDashboardConfig returns a populated PluginDashboard struct
|
||||
func DummyPluginDashboardConfig() configs.PluginDashboard {
|
||||
return configs.PluginDashboard{
|
||||
Protocol: "http",
|
||||
FQDN: "localhost",
|
||||
Port: 80,
|
||||
Path: "index.html",
|
||||
// DummyDashboardsConfig returns an array of populated Dashboard structs
|
||||
func DummyDashboardsConfig() []configs.Dashboard {
|
||||
e := DummyExecutableConfig()
|
||||
return []configs.Dashboard{
|
||||
{
|
||||
Name: "dummy_dashboard",
|
||||
BaseURL: "http://dummyhost",
|
||||
Path: "fake/login/path",
|
||||
Executable: e,
|
||||
},
|
||||
{
|
||||
Name: "dummy_plugin_no_dash",
|
||||
Executable: e,
|
||||
},
|
||||
{
|
||||
Name: "dummy_dashboard_no_exe",
|
||||
BaseURL: "http://dummyhost",
|
||||
Path: "fake/login/path",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// DummyExecutableConfig returns a populated Executable struct
|
||||
func DummyExecutableConfig() configs.Executable {
|
||||
return configs.Executable{
|
||||
func DummyExecutableConfig() *configs.Executable {
|
||||
return &configs.Executable{
|
||||
AutoStart: true,
|
||||
Filepath: "/fake/path/to/executable",
|
||||
Args: []string{
|
||||
@ -122,77 +134,21 @@ func DummyAuthMethodConfig() *configs.AuthMethod {
|
||||
}
|
||||
}
|
||||
|
||||
// DummyPluginWithDashboardConfig returns a populated Plugin struct
|
||||
// with a populated PluginDashboard
|
||||
func DummyPluginWithDashboardConfig() configs.Plugin {
|
||||
d := DummyPluginDashboardConfig()
|
||||
e := DummyExecutableConfig()
|
||||
|
||||
return configs.Plugin{
|
||||
Name: "dummy_plugin_with_dash",
|
||||
Dashboard: &d,
|
||||
Executable: &e,
|
||||
}
|
||||
}
|
||||
|
||||
// DummyPluginNoDashboard returns a populated Plugin struct
|
||||
// but omits the optional PluginDashboard
|
||||
func DummyPluginNoDashboard() configs.Plugin {
|
||||
e := DummyExecutableConfig()
|
||||
|
||||
return configs.Plugin{
|
||||
Name: "dummy_plugin_no_dash",
|
||||
Executable: &e,
|
||||
}
|
||||
}
|
||||
|
||||
// DummyNamespaceConfig returns a populated Namespace struct with
|
||||
// a single Dashboard
|
||||
func DummyNamespaceConfig() configs.Namespace {
|
||||
d := DummyDashboardConfig()
|
||||
|
||||
return configs.Namespace{
|
||||
Name: "dummy_namespace",
|
||||
Dashboards: []configs.Dashboard{d},
|
||||
}
|
||||
}
|
||||
|
||||
// DummyClusterConfig returns a populated Cluster struct with
|
||||
// a single Namespace
|
||||
func DummyClusterConfig() configs.Cluster {
|
||||
n := DummyNamespaceConfig()
|
||||
|
||||
return configs.Cluster{
|
||||
Name: "dummy_cluster",
|
||||
BaseFqdn: "dummy.cluster.local",
|
||||
Namespaces: []configs.Namespace{n},
|
||||
}
|
||||
}
|
||||
|
||||
// DummyConfigNoAuth returns a populated Config struct but omits
|
||||
// the optional AuthMethod
|
||||
func DummyConfigNoAuth() configs.Config {
|
||||
p := DummyPluginWithDashboardConfig()
|
||||
pn := DummyPluginNoDashboard()
|
||||
c := DummyClusterConfig()
|
||||
d := DummyDashboardConfig()
|
||||
|
||||
return configs.Config{
|
||||
Plugins: []configs.Plugin{p, pn},
|
||||
Clusters: []configs.Cluster{c},
|
||||
Dashboards: []configs.Dashboard{d},
|
||||
}
|
||||
}
|
||||
|
||||
// DummyCompleteConfig returns a fully populated Config struct
|
||||
func DummyCompleteConfig() configs.Config {
|
||||
a := DummyAuthMethodConfig()
|
||||
p := DummyPluginWithDashboardConfig()
|
||||
pn := DummyPluginNoDashboard()
|
||||
c := DummyClusterConfig()
|
||||
|
||||
return configs.Config{
|
||||
AuthMethod: a,
|
||||
Plugins: []configs.Plugin{p, pn},
|
||||
Clusters: []configs.Cluster{c},
|
||||
AuthMethod: DummyAuthMethodConfig(),
|
||||
Dashboards: DummyDashboardsConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
|
BIN
web/favorite.ico
Executable file
BIN
web/favorite.ico
Executable file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
@ -43,14 +43,6 @@
|
||||
<li><a class="c-sidebar-nav-link" id="LiDocument" onclick="ctlGetDefaults(this)">Document</a></span></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="c-sidebar-nav-item c-sidebar-nav-dropdown"><a
|
||||
class="c-sidebar-nav-link c-sidebar-nav-dropdown-toggle" href="#">
|
||||
<svg class="c-sidebar-nav-icon">
|
||||
<use xlink:href="node_modules/@coreui/icons/sprites/free.svg#cil-puzzle"></use>
|
||||
</svg> Plugins</a>
|
||||
<ul id="PluginDropdown" class="c-sidebar-nav-dropdown-items">
|
||||
</ul>
|
||||
</li>
|
||||
<li class="c-sidebar-nav-item c-sidebar-nav-dropdown"><a
|
||||
class="c-sidebar-nav-link c-sidebar-nav-dropdown-toggle" href="#">
|
||||
<svg class="c-sidebar-nav-icon">
|
||||
|
@ -36,14 +36,12 @@ function documentAction(element) { // eslint-disable-line no-unused-vars
|
||||
Object.assign(json, { "subComponent": "yamlWrite" });
|
||||
Object.assign(json, { "message": editorContents });
|
||||
Object.assign(json, { "yaml": window.btoa(editor.getValue()) });
|
||||
console.log(json);
|
||||
break;
|
||||
}
|
||||
ws.send(JSON.stringify(json));
|
||||
}
|
||||
|
||||
function ctlParseDocument(json) { // eslint-disable-line no-unused-vars
|
||||
console.log(json["subComponent"]);
|
||||
switch(json["subComponent"]) {
|
||||
case "getDefaults": displayCTLInfo(json); addFolderToggles(); break;
|
||||
case "yaml": insertEditor(json); break;
|
||||
|
@ -98,44 +98,18 @@ function insertGraph(data) { // eslint-disable-line no-unused-vars
|
||||
}
|
||||
|
||||
// add dashboard links to Dropdown if present in $HOME/.airship/airshipui.json
|
||||
function addServiceDashboards(json) { // eslint-disable-line no-unused-vars
|
||||
function addDashboards(json) { // eslint-disable-line no-unused-vars
|
||||
if (json !== undefined) {
|
||||
for (let i = 0; i < json.length; i++) {
|
||||
let cluster = json[i];
|
||||
for (let j = 0; j < cluster.namespaces.length; j++) {
|
||||
let namespace = cluster.namespaces[j];
|
||||
for (let k = 0; k < namespace.dashboards.length; k++) {
|
||||
let dash = namespace.dashboards[k];
|
||||
let fqdn = null;
|
||||
if (dash.hasOwnProperty("fqdn")) {
|
||||
fqdn = `${dash.fqdn}`;
|
||||
} else {
|
||||
fqdn = `${dash.hostname}.${cluster.namespaces[j].name}.${cluster.baseFqdn}`;
|
||||
}
|
||||
let url = `${dash.protocol}://${fqdn}:${dash.port}/${dash.path || ""}`;
|
||||
addDashboard("DashDropdown", dash.name, url)
|
||||
}
|
||||
}
|
||||
let dashboard = json[i];
|
||||
let url = `${dashboard.baseURL}/${dashboard.path || ""}`;
|
||||
addDashboard(dashboard.name, url, dashboard.isProxied)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if any plugins (external executables) have a corresponding web dashboard defined,
|
||||
// add them to the dropdown
|
||||
function addPluginDashboards(json) { // eslint-disable-line no-unused-vars
|
||||
if (json !== undefined) {
|
||||
for (let i = 0; i < json.length; i++) {
|
||||
if (json[i].executable.autoStart && json[i].dashboard !== undefined) {
|
||||
let dash = json[i].dashboard;
|
||||
let url = `${dash.protocol}://${dash.fqdn}:${dash.port}/${dash.path || ""}`;
|
||||
addDashboard("PluginDropdown", json[i].name, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addDashboard(navElement, name, url) {
|
||||
let nav = document.getElementById(navElement);
|
||||
function addDashboard(name, url, proxied) {
|
||||
let nav = document.getElementById("DashDropdown");
|
||||
let li = document.createElement("li");
|
||||
li.className = "c-sidebar-nav-item";
|
||||
let a = document.createElement("a");
|
||||
@ -143,15 +117,20 @@ function addDashboard(navElement, name, url) {
|
||||
let span = document.createElement("span");
|
||||
span.className = "c-sidebar-nav-icon";
|
||||
a.innerText = name;
|
||||
a.appendChild(span);
|
||||
li.appendChild(a);
|
||||
nav.appendChild(li);
|
||||
if (proxied) {
|
||||
a.target = "_blank";
|
||||
a.href = "javascript:window.open('" + url + "')";
|
||||
} else {
|
||||
a.onclick = () => {
|
||||
let view = document.getElementById("DashView");
|
||||
view.src = url;
|
||||
document.getElementById("ContentDiv").style.display = "none";
|
||||
document.getElementById("DashView").style.display = "";
|
||||
}
|
||||
a.appendChild(span);
|
||||
li.appendChild(a);
|
||||
nav.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
function authenticate(json) { // eslint-disable-line no-unused-vars
|
||||
|
@ -28,6 +28,10 @@ function createWindow () {
|
||||
// disable the default menu bar
|
||||
//win.setMenu(null);
|
||||
|
||||
win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
callback({responseHeaders: Object.fromEntries(Object.entries(details.responseHeaders).filter(header => !/x-frame-options/i.test(header[0])))});
|
||||
});
|
||||
|
||||
// and load the index.html of the app.
|
||||
win.loadFile('index.html')
|
||||
|
||||
|
@ -76,11 +76,8 @@ function hanldleAirshipUIMessages(json) {
|
||||
} else {
|
||||
authComplete();
|
||||
}
|
||||
if (json.hasOwnProperty("plugins")) {
|
||||
addPluginDashboards(json["plugins"]);
|
||||
}
|
||||
if (json.hasOwnProperty("dashboards")) {
|
||||
addServiceDashboards(json["dashboards"]);
|
||||
addDashboards(json["dashboards"]);
|
||||
}
|
||||
} else if (json["component"] === "authcomplete") {
|
||||
authComplete();
|
||||
|
6
web/package-lock.json
generated
6
web/package-lock.json
generated
@ -667,9 +667,9 @@
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
|
||||
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.2.tgz",
|
||||
"integrity": "sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw==",
|
||||
"dev": true
|
||||
},
|
||||
"env-paths": {
|
||||
|
@ -146,3 +146,11 @@
|
||||
padding:0px;
|
||||
opacity:97%;
|
||||
}
|
||||
|
||||
/* iframe itself */
|
||||
div#embedded-dashboard > iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user