Merge "Add testing to airshipui"

This commit is contained in:
Zuul 2020-06-22 21:21:47 +00:00 committed by Gerrit Code Review
commit afa71c0997
26 changed files with 1365 additions and 224 deletions

2
go.mod
View File

@ -7,9 +7,11 @@ require (
github.com/gorilla/websocket v1.4.2
github.com/spf13/cobra v0.0.6
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.4.0
github.com/vmware-tanzu/octant v0.12.0
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527
gopkg.in/src-d/go-git.v4 v4.13.1
k8s.io/api v0.17.4
k8s.io/apimachinery v0.17.4
opendev.org/airship/airshipctl v0.0.0-20200518155418-7276dd68d8d0

View File

@ -20,6 +20,7 @@ import (
"log"
"os"
"os/signal"
"path/filepath"
"sync"
"syscall"
@ -51,6 +52,13 @@ func init() {
}
func launch(cmd *cobra.Command, args []string) {
// set default config path
// TODO: do we want to make this a flag that can be passed in?
airshipUIConfigPath, err := getDefaultConfigPath()
if err != nil {
log.Printf("Error setting config path %s", err)
}
sigs := make(chan os.Signal)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
@ -59,7 +67,7 @@ func launch(cmd *cobra.Command, args []string) {
waitgrp := sync.WaitGroup{}
// Read AirshipUI config file
if err := configs.GetConfigFromFile(); err == nil {
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 {
@ -134,3 +142,12 @@ func Execute() {
os.Exit(1)
}
}
func getDefaultConfigPath() (string, error) {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.FromSlash(home + "/.airship/airshipui.json"), nil
}

View File

@ -18,7 +18,6 @@ import (
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"opendev.org/airship/airshipctl/pkg/config"
)
@ -30,9 +29,9 @@ 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"`
AuthMethod *AuthMethod `json:"authMethod,omitempty"`
Plugins []Plugin `json:"plugins,omitempty"`
Clusters []Cluster `json:"clusters,omitempty"`
}
// AuthMethod structure to hold authentication parameters
@ -44,18 +43,24 @@ type AuthMethod struct {
// Plugin structure to hold plugin specific parameters
type Plugin struct {
Name string `json:"name,omitempty"`
Dashboard struct {
Protocol string `json:"protocol,omitempty"`
FQDN string `json:"fqdn,omitempty"`
Port uint16 `json:"port,omitempty"`
Path string `json:"path,omitempty"`
} `json:"dashboard"`
Executable struct {
AutoStart bool `json:"autoStart,omitempty"`
Filepath string `json:"filepath,omitempty"`
Args []string `json:"args,omitempty"`
} `json:"executable"`
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"`
Filepath string `json:"filepath,omitempty"`
Args []string `json:"args,omitempty"`
}
// Dashboard structure
@ -132,39 +137,42 @@ type WsMessage struct {
Message string `json:"message,omitempty"`
// information related to the init of the UI
Dashboards []Cluster `json:"dashboards,omitempty"`
Plugins []Plugin `json:"plugins,omitempty"`
Authentication AuthMethod `json:"authentication,omitempty"`
AuthInfoOptions config.AuthInfoOptions `json:"authInfoOptions,omitempty"`
ContextOptions config.ContextOptions `json:"contextOptions,omitempty"`
ClusterOptions config.ClusterOptions `json:"clusterOptions,omitempty"`
Dashboards []Cluster `json:"dashboards,omitempty"`
Plugins []Plugin `json:"plugins,omitempty"`
Authentication *AuthMethod `json:"authentication,omitempty"`
AuthInfoOptions *config.AuthInfoOptions `json:"authInfoOptions,omitempty"`
ContextOptions *config.ContextOptions `json:"contextOptions,omitempty"`
ClusterOptions *config.ClusterOptions `json:"clusterOptions,omitempty"`
}
// GetConfigFromFile reads configuration file and returns error on any error reading the file
// SetUIConfig sets the UIConfig object with values obtained from
// airshipui.json, located at 'filename'
// TODO: add watcher to the json file to reload conf on change
func GetConfigFromFile() error {
var fileName string
home, err := os.UserHomeDir()
func SetUIConfig(filename string) error {
bytes, err := getBytesFromFile(filename)
if err != nil {
return err
}
fileName = filepath.FromSlash(home + "/.airship/airshipui.json")
jsonFile, err := os.Open(fileName)
err = json.Unmarshal(bytes, &UIConfig)
if err != nil {
return err
}
defer jsonFile.Close()
byteValue, err := ioutil.ReadAll(jsonFile)
if err != nil {
return err
}
err = json.Unmarshal(byteValue, &UIConfig)
return err
return nil
}
func getBytesFromFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
bytes, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
return bytes, nil
}

View File

@ -0,0 +1,51 @@
/*
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 configs_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipui/internal/configs"
"opendev.org/airship/airshipui/testutil"
)
const (
fakeFile string = "/fake/config/path"
testFile string = "testdata/airshipui.json"
)
func TestSetUIConfig(t *testing.T) {
conf := configs.Config{
Clusters: []configs.Cluster{
testutil.DummyClusterConfig(),
},
Plugins: []configs.Plugin{
testutil.DummyPluginWithDashboardConfig(),
testutil.DummyPluginNoDashboard(),
},
AuthMethod: testutil.DummyAuthMethodConfig(),
}
err := configs.SetUIConfig(testFile)
require.NoError(t, err)
assert.Equal(t, conf, configs.UIConfig)
}
func TestFileNotFound(t *testing.T) {
err := configs.SetUIConfig(fakeFile)
assert.Error(t, err)
}

View File

@ -0,0 +1,55 @@
{
"authMethod": {
"url": "http://fake.auth.method.com/auth"
},
"plugins": [
{
"name": "dummy_plugin_with_dash",
"dashboard": {
"protocol": "http",
"fqdn": "localhost",
"port": 80,
"path": "index.html"
},
"executable": {
"autoStart": true,
"filepath": "/fake/path/to/executable",
"args": [
"--fakeflag",
"fakevalue"
]
}
},
{
"name": "dummy_plugin_no_dash",
"executable": {
"autoStart": true,
"filepath": "/fake/path/to/executable",
"args": [
"--fakeflag",
"fakevalue"
]
}
}
],
"clusters": [
{
"name": "dummy_cluster",
"baseFqdn": "dummy.cluster.local",
"namespaces": [
{
"name": "dummy_namespace",
"dashboards": [
{
"name": "dummy_dashboard",
"protocol": "http",
"hostname": "dummyhost",
"port": 80,
"path": "fake/login/path"
}
]
}
]
}
]
}

View File

@ -16,6 +16,8 @@ package ctl
import (
"bytes"
"path/filepath"
"runtime"
"text/template"
"opendev.org/airship/airshipctl/pkg/environment"
@ -23,6 +25,13 @@ import (
"opendev.org/airship/airshipui/internal/configs"
)
// obtain base path of caller so references to html
// template files still work from outside the package
var (
_, b, _, _ = runtime.Caller(0)
basepath = filepath.Dir(b)
)
// maintain the state of a potentially long running process
var runningRequests map[configs.WsSubComponentType]bool = make(map[configs.WsSubComponentType]bool)

View File

@ -16,6 +16,7 @@ package ctl
import (
"fmt"
"path/filepath"
"opendev.org/airship/airshipctl/pkg/bootstrap/isogen"
"opendev.org/airship/airshipui/internal/configs"
@ -77,5 +78,5 @@ func getBaremetalHTML() (string, error) {
p.ButtonText = "In Progress"
}
return getHTML("./internal/integrations/ctl/templates/baremetal.html", p)
return getHTML(filepath.Join(basepath, "/templates/baremetal.html"), p)
}

View File

@ -0,0 +1,69 @@
/*
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 ctl
import (
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipui/internal/configs"
)
const (
testBaremetalHTML string = "testdata/baremetal.html"
)
func TestHandleDefaultBaremetalRequest(t *testing.T) {
html, err := ioutil.ReadFile(testBaremetalHTML)
require.NoError(t, err)
request := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.Baremetal,
SubComponent: configs.GetDefaults,
}
response := HandleBaremetalRequest(request)
expected := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.Baremetal,
SubComponent: configs.GetDefaults,
HTML: string(html),
}
assert.Equal(t, expected, response)
}
func TestHandleUnknownBaremetalSubComponent(t *testing.T) {
request := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.Baremetal,
SubComponent: "fake_subcomponent",
}
response := HandleBaremetalRequest(request)
expected := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.Baremetal,
SubComponent: "fake_subcomponent",
Error: "Subcomponent fake_subcomponent not found",
}
assert.Equal(t, expected, response)
}

View File

@ -16,6 +16,7 @@ package ctl
import (
"fmt"
"path/filepath"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipui/internal/configs"
@ -126,7 +127,7 @@ func getCredentialTableRows() string {
}
func getConfigHTML() (string, error) {
return getHTML("./internal/integrations/ctl/templates/config.html", ctlPage{
return getHTML(filepath.Join(basepath, "/templates/config.html"), ctlPage{
ClusterRows: getClusterTableRows(),
ContextRows: getContextTableRows(),
CredentialRows: getCredentialTableRows(),
@ -137,7 +138,7 @@ func getConfigHTML() (string, error) {
// SetCluster will take ui cluster info, translate them into CTL commands and send a response back to the UI
func setCluster(request configs.WsMessage) (string, error) {
modified, err := config.RunSetCluster(&request.ClusterOptions, c.settings.Config, true)
modified, err := config.RunSetCluster(request.ClusterOptions, c.settings.Config, true)
var message string
if modified {
@ -153,7 +154,7 @@ func setCluster(request configs.WsMessage) (string, error) {
// SetContext will take ui context info, translate them into CTL commands and send a response back to the UI
func setContext(request configs.WsMessage) (string, error) {
modified, err := config.RunSetContext(&request.ContextOptions, c.settings.Config, true)
modified, err := config.RunSetContext(request.ContextOptions, c.settings.Config, true)
var message string
if modified {
@ -167,7 +168,7 @@ func setContext(request configs.WsMessage) (string, error) {
// SetContext will take ui context info, translate them into CTL commands and send a response back to the UI
func setCredential(request configs.WsMessage) (string, error) {
modified, err := config.RunSetAuthInfo(&request.AuthInfoOptions, c.settings.Config, true)
modified, err := config.RunSetAuthInfo(request.AuthInfoOptions, c.settings.Config, true)
var message string
if modified {

View File

@ -0,0 +1,88 @@
/*
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 ctl
import (
"io/ioutil"
"testing"
"opendev.org/airship/airshipui/internal/configs"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/environment"
)
const (
testConfigHTML string = "testdata/config.html"
testKubeConfig string = "testdata/kubeconfig.yaml"
testAirshipConfig string = "testdata/config.yaml"
)
func TestHandleDefaultConfigRequest(t *testing.T) {
html, err := ioutil.ReadFile(testConfigHTML)
require.NoError(t, err)
// point airshipctl client toward test configs
c.settings = &environment.AirshipCTLSettings{
AirshipConfigPath: testAirshipConfig,
KubeConfigPath: testKubeConfig,
Config: config.NewConfig(),
}
err = c.settings.Config.LoadConfig(
c.settings.AirshipConfigPath,
c.settings.KubeConfigPath,
)
require.NoError(t, err)
// simulate incoming WsMessage from websocket client
request := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.CTLConfig,
SubComponent: configs.GetDefaults,
}
response := HandleConfigRequest(request)
expected := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.CTLConfig,
SubComponent: configs.GetDefaults,
HTML: string(html),
}
assert.Equal(t, expected, response)
}
func TestHandleUnknownConfigSubComponent(t *testing.T) {
request := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.CTLConfig,
SubComponent: "fake_subcomponent",
}
response := HandleConfigRequest(request)
expected := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.CTLConfig,
SubComponent: "fake_subcomponent",
Error: "Subcomponent fake_subcomponent not found",
}
assert.Equal(t, expected, response)
}

View File

@ -16,6 +16,7 @@ package ctl
import (
"fmt"
"path/filepath"
"opendev.org/airship/airshipctl/pkg/document/pull"
"opendev.org/airship/airshipui/internal/configs"
@ -61,7 +62,7 @@ func (c *Client) docPull() (string, error) {
}
func getDocumentHTML() (string, error) {
return getHTML("./internal/integrations/ctl/templates/document.html", ctlPage{
return getHTML(filepath.Join(basepath, "/templates/document.html"), ctlPage{
Title: "Document",
Version: getAirshipCTLVersion(),
})

View File

@ -0,0 +1,69 @@
/*
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 ctl
import (
"io/ioutil"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipui/internal/configs"
)
const (
testDocumentHTML string = "testdata/document.html"
)
func TestHandleDefaultDocumentRequest(t *testing.T) {
html, err := ioutil.ReadFile(testDocumentHTML)
require.NoError(t, err)
request := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.Document,
SubComponent: configs.GetDefaults,
}
response := HandleDocumentRequest(request)
expected := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.Document,
SubComponent: configs.GetDefaults,
HTML: string(html),
}
assert.Equal(t, expected, response)
}
func TestHandleUnknownDocumentSubComponent(t *testing.T) {
request := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.Document,
SubComponent: "fake_subcomponent",
}
response := HandleDocumentRequest(request)
expected := configs.WsMessage{
Type: configs.AirshipCTL,
Component: configs.Document,
SubComponent: "fake_subcomponent",
Error: "Subcomponent fake_subcomponent not found",
}
assert.Equal(t, expected, response)
}

View File

@ -1,5 +1,5 @@
<h1>Airship CTL {{.Title}} Base Information</h1>
<p>Version: {{.Version}}</p>
<h2>Generate ISO</h2>
<button type="button" class="btn btn-info" id="GenIsoBtn" onclick="baremetalAction(this)" style="width: 150px;" {{.Disabled}}>{{.ButtonText}}</button>
<h1>Airship CTL {{.Title}} Base Information</h1>
<p>Version: {{.Version}}</p>
<h2>Generate ISO</h2>
<button type="button" class="btn btn-info" id="GenIsoBtn" onclick="baremetalAction(this)" style="width: 150px;" {{.Disabled}}>{{.ButtonText}}</button>

View File

@ -1,162 +1,160 @@
<h1>Airship CTL {{.Title}} Base Information</h1>
<p>Version: {{.Version}}</p>
<!-- Cluster details in accordion -->
<button class="accordion">Cluster</button>
<div class="panel">
<p>
<table class="table" id="ClusterTable">
<thead>
<tr>
<th scope="col">Bootstrap Info</th>
<th scope="col">Cluster Kube Conf</th>
<th scope="col">Management Configuration</th>
<th scope="col">Location Of Origin</th>
<th scope="col">Server</th>
<th scope="col">Certificate Authority</th>
<th></th>
</tr>
</thead>
<tbody>
{{.ClusterRows}}
</tbody>
</table>
<button type="button" class="btn btn-info" id="ClusterBtn" onclick="addConfigModal(this)">Add</button>
</p>
</div>
<!-- This is used by the cluster add modal and is injected into the DOM but not displayed except by the popup -->
<div id="ClusterModalTemplate" style="display:none">
<h2>Add Cluster Member</h2>
<table class="table" id="ClusterAddTable">
<thead>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Type</th>
<th scope="col">Server</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text"></td>
<td>
<select>
<option value="ephemeral">ephemeral</option>
<option value="target">target</option>
</select>
</td>
<td><input type="text"></td>
<td>
<button class="btn btn-success" onclick="saveConfigDialog(this)">Save</button>&nbsp;&nbsp;
<button class="btn btn-dark" onclick="closeDialog(this)">Cancel</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Context details in accordion -->
<button class="accordion">Context</button>
<div class="panel">
<p>
<table class="table" id="ContextTable">
<thead>
<tr>
<th scope="col">Context Kube Conf</th>
<th scope="col">Manifest</th>
<th scope="col">Location Of Origin</th>
<th scope="col">Cluster</th>
<th scope="col">User</th>
<th></th>
</tr>
</thead>
<tbody>
{{.ContextRows}}
</tbody>
</table>
</p>
<button type="button" class="btn btn-info" id="ContextBtn" onclick="addConfigModal(this)">Add</button>
</div>
<!-- This is used by the context add modal and is injected into the DOM but not displayed except by the popup -->
<div id="ContextModalTemplate" style="display:none">
<h2>Add Context</h2>
<table class="table" id="ContextAddTable">
<thead>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Type</th>
<th scope="col">Cluster</th>
<th scope="col">User</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text"></td>
<td>
<select>
<option value="ephemeral">ephemeral</option>
<option value="target">target</option>
</select>
</td>
<td><input type="text"></td>
<td><input type="text"></td>
<td>
<button class="btn btn-success" onclick="saveConfigDialog(this)">Save</button>&nbsp;&nbsp;
<button class="btn btn-dark" onclick="closeDialog(this)">Cancel</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Credential details in accordion -->
<button class="accordion">Credential</button>
<div class="panel">
<p>
<table class="table" id="CredentialTable">
<thead>
<tr>
<th scope="col">Location Of Origin</th>
<th scope="col">Username</th>
<th></th>
</tr>
</thead>
<tbody>
{{.CredentialRows}}
</tbody>
</table>
<button type="button" class="btn btn-info" id="CredentialBtn" onclick="addConfigModal(this)">Add</button>
</p>
</div>
<!-- This is used by the credential add modal and is injected into the DOM but not displayed except by the popup -->
<div id="CredentialModalTemplate" style="display:none">
<h2>Add Credential</h2>
<table class="table" id="CredentialAddTable">
<thead>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">User</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text"></td>
<td><input type="text"></td>
<td>
<button class="btn btn-success" onclick="saveConfigDialog(this)">Save</button>&nbsp;&nbsp;
<button class="btn btn-dark" onclick="closeDialog(this)">Cancel</button>
</td>
</tr>
</tbody>
</table>
</div>
<h1>Airship CTL {{.Title}} Base Information</h1>
<p>Version: {{.Version}}</p>
<!-- Cluster details in accordion -->
<button class="accordion">Cluster</button>
<div class="panel">
<p>
<table class="table" id="ClusterTable">
<thead>
<tr>
<th scope="col">Bootstrap Info</th>
<th scope="col">Cluster Kube Conf</th>
<th scope="col">Management Configuration</th>
<th scope="col">Location Of Origin</th>
<th scope="col">Server</th>
<th scope="col">Certificate Authority</th>
<th></th>
</tr>
</thead>
<tbody>
{{.ClusterRows}}
</tbody>
</table>
<button type="button" class="btn btn-info" id="ClusterBtn" onclick="addConfigModal(this)">Add</button>
</p>
</div>
<!-- This is used by the cluster add modal and is injected into the DOM but not displayed except by the popup -->
<div id="ClusterModalTemplate" style="display:none">
<h2>Add Cluster Member</h2>
<table class="table" id="ClusterAddTable">
<thead>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Type</th>
<th scope="col">Server</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text"></td>
<td>
<select>
<option value="ephemeral">ephemeral</option>
<option value="target">target</option>
</select>
</td>
<td><input type="text"></td>
<td>
<button class="btn btn-success" onclick="saveConfigDialog(this)">Save</button>&nbsp;&nbsp;
<button class="btn btn-dark" onclick="closeDialog(this)">Cancel</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Context details in accordion -->
<button class="accordion">Context</button>
<div class="panel">
<p>
<table class="table" id="ContextTable">
<thead>
<tr>
<th scope="col">Context Kube Conf</th>
<th scope="col">Manifest</th>
<th scope="col">Location Of Origin</th>
<th scope="col">Cluster</th>
<th scope="col">User</th>
<th></th>
</tr>
</thead>
<tbody>
{{.ContextRows}}
</tbody>
</table>
</p>
<button type="button" class="btn btn-info" id="ContextBtn" onclick="addConfigModal(this)">Add</button>
</div>
<!-- This is used by the context add modal and is injected into the DOM but not displayed except by the popup -->
<div id="ContextModalTemplate" style="display:none">
<h2>Add Context</h2>
<table class="table" id="ContextAddTable">
<thead>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Type</th>
<th scope="col">Cluster</th>
<th scope="col">User</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text"></td>
<td>
<select>
<option value="ephemeral">ephemeral</option>
<option value="target">target</option>
</select>
</td>
<td><input type="text"></td>
<td><input type="text"></td>
<td>
<button class="btn btn-success" onclick="saveConfigDialog(this)">Save</button>&nbsp;&nbsp;
<button class="btn btn-dark" onclick="closeDialog(this)">Cancel</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Credential details in accordion -->
<button class="accordion">Credential</button>
<div class="panel">
<p>
<table class="table" id="CredentialTable">
<thead>
<tr>
<th scope="col">Location Of Origin</th>
<th scope="col">Username</th>
<th></th>
</tr>
</thead>
<tbody>
{{.CredentialRows}}
</tbody>
</table>
<button type="button" class="btn btn-info" id="CredentialBtn" onclick="addConfigModal(this)">Add</button>
</p>
</div>
<!-- This is used by the credential add modal and is injected into the DOM but not displayed except by the popup -->
<div id="CredentialModalTemplate" style="display:none">
<h2>Add Credential</h2>
<table class="table" id="CredentialAddTable">
<thead>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">User</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text"></td>
<td><input type="text"></td>
<td>
<button class="btn btn-success" onclick="saveConfigDialog(this)">Save</button>&nbsp;&nbsp;
<button class="btn btn-dark" onclick="closeDialog(this)">Cancel</button>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -1,5 +1,5 @@
<h1>Airship CTL {{.Title}} Base Information</h1>
<p>Version: {{.Version}}</p>
<h2>Document Pull</h2>
<button type="button" class="btn btn-info" id="DocPullBtn" onclick="documentAction(this)" style="width: 150px;">Document Pull</button>
<h1>Airship CTL {{.Title}} Base Information</h1>
<p>Version: {{.Version}}</p>
<h2>Document Pull</h2>
<button type="button" class="btn btn-info" id="DocPullBtn" onclick="documentAction(this)" style="width: 150px;">Document Pull</button>

View File

@ -0,0 +1,5 @@
<h1>Airship CTL Baremetal Base Information</h1>
<p>Version: devel</p>
<h2>Generate ISO</h2>
<button type="button" class="btn btn-info" id="GenIsoBtn" onclick="baremetalAction(this)" style="width: 150px;" >Generate ISO</button>

View File

@ -0,0 +1,160 @@
<h1>Airship CTL Config Base Information</h1>
<p>Version: devel</p>
<!-- Cluster details in accordion -->
<button class="accordion">Cluster</button>
<div class="panel">
<p>
<table class="table" id="ClusterTable">
<thead>
<tr>
<th scope="col">Bootstrap Info</th>
<th scope="col">Cluster Kube Conf</th>
<th scope="col">Management Configuration</th>
<th scope="col">Location Of Origin</th>
<th scope="col">Server</th>
<th scope="col">Certificate Authority</th>
<th></th>
</tr>
</thead>
<tbody>
<tr><td><div contenteditable=true>default</div></td><td><div contenteditable=true>kubernetes_target</div></td><td><div contenteditable=true>default</div></td><td>testdata/kubeconfig.yaml</td><td><div contenteditable=true>https://10.0.0.1:6553</div></td><td><div contenteditable=true>pki/cluster-ca.pem</div></td><td><button type="button" class="btn btn-success" onclick="saveConfig(this)">Save</button></td></tr>
</tbody>
</table>
<button type="button" class="btn btn-info" id="ClusterBtn" onclick="addConfigModal(this)">Add</button>
</p>
</div>
<!-- This is used by the cluster add modal and is injected into the DOM but not displayed except by the popup -->
<div id="ClusterModalTemplate" style="display:none">
<h2>Add Cluster Member</h2>
<table class="table" id="ClusterAddTable">
<thead>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Type</th>
<th scope="col">Server</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text"></td>
<td>
<select>
<option value="ephemeral">ephemeral</option>
<option value="target">target</option>
</select>
</td>
<td><input type="text"></td>
<td>
<button class="btn btn-success" onclick="saveConfigDialog(this)">Save</button>&nbsp;&nbsp;
<button class="btn btn-dark" onclick="closeDialog(this)">Cancel</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Context details in accordion -->
<button class="accordion">Context</button>
<div class="panel">
<p>
<table class="table" id="ContextTable">
<thead>
<tr>
<th scope="col">Context Kube Conf</th>
<th scope="col">Manifest</th>
<th scope="col">Location Of Origin</th>
<th scope="col">Cluster</th>
<th scope="col">User</th>
<th></th>
</tr>
</thead>
<tbody>
<tr><td><div contenteditable=true>kubernetes_target</div></td><td><div contenteditable=true></div></td><td>testdata/kubeconfig.yaml</td><td><div contenteditable=true>kubernetes_target</div></td><td><div contenteditable=true>admin</div></td><td><button type="button" class="btn btn-success" onclick="saveConfig(this)">Save</button></td></tr>
</tbody>
</table>
</p>
<button type="button" class="btn btn-info" id="ContextBtn" onclick="addConfigModal(this)">Add</button>
</div>
<!-- This is used by the context add modal and is injected into the DOM but not displayed except by the popup -->
<div id="ContextModalTemplate" style="display:none">
<h2>Add Context</h2>
<table class="table" id="ContextAddTable">
<thead>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Type</th>
<th scope="col">Cluster</th>
<th scope="col">User</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text"></td>
<td>
<select>
<option value="ephemeral">ephemeral</option>
<option value="target">target</option>
</select>
</td>
<td><input type="text"></td>
<td><input type="text"></td>
<td>
<button class="btn btn-success" onclick="saveConfigDialog(this)">Save</button>&nbsp;&nbsp;
<button class="btn btn-dark" onclick="closeDialog(this)">Cancel</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Credential details in accordion -->
<button class="accordion">Credential</button>
<div class="panel">
<p>
<table class="table" id="CredentialTable">
<thead>
<tr>
<th scope="col">Location Of Origin</th>
<th scope="col">Username</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<button type="button" class="btn btn-info" id="CredentialBtn" onclick="addConfigModal(this)">Add</button>
</p>
</div>
<!-- This is used by the credential add modal and is injected into the DOM but not displayed except by the popup -->
<div id="CredentialModalTemplate" style="display:none">
<h2>Add Credential</h2>
<table class="table" id="CredentialAddTable">
<thead>
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">User</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td><input type="text"></td>
<td><input type="text"></td>
<td>
<button class="btn btn-success" onclick="saveConfigDialog(this)">Save</button>&nbsp;&nbsp;
<button class="btn btn-dark" onclick="closeDialog(this)">Cancel</button>
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,73 @@
apiVersion: airshipit.org/v1alpha1
bootstrapInfo:
default:
builder:
networkConfigFileName: network-config
outputMetadataFileName: output-metadata.yaml
userDataFileName: user-data
container:
containerRuntime: docker
image: quay.io/airshipit/isogen:latest-debian_stable
volume: /srv/iso:/config
remoteDirect:
isoUrl: http://localhost:8099/debian-custom.iso
dummy_bootstrap_config:
builder:
networkConfigFileName: netconfig
outputMetadataFileName: output-metadata.yaml
userDataFileName: user-data
container:
containerRuntime: docker
image: dummy_image:dummy_tag
volume: /dummy:dummy
clusters:
kubernetes:
clusterType:
target:
bootstrapInfo: default
clusterKubeconf: kubernetes_target
managementConfiguration: default
contexts:
admin@kubernetes:
contextKubeconf: kubernetes_target
currentContext: admin@kubernetes
kind: Config
managementConfiguration:
default:
insecure: true
systemActionRetries: 30
systemRebootDelay: 30
type: redfish
dummy_management_config:
insecure: true
type: redfish
manifests:
default:
primaryRepositoryName: primary
repositories:
primary:
checkout:
branch: master
commitHash: ""
force: false
tag: ""
url: https://opendev.org/airship/treasuremap
subPath: treasuremap/manifests/site
targetPath: /tmp/default
dummy_manifest:
primaryRepositoryName: primary
repositories:
primary:
auth:
sshKey: testdata/test-key.pem
type: ssh-key
checkout:
branch: ""
commitHash: ""
force: false
tag: v1.0.1
url: http://dummy.url.com/manifests.git
subPath: manifests/site/test-site
targetPath: /var/tmp/
users:
admin: {}

View File

@ -0,0 +1,5 @@
<h1>Airship CTL Document Base Information</h1>
<p>Version: devel</p>
<h2>Document Pull</h2>
<button type="button" class="btn btn-info" id="DocPullBtn" onclick="documentAction(this)" style="width: 150px;">Document Pull</button>

View File

@ -0,0 +1,19 @@
apiVersion: v1
clusters:
- cluster:
certificate-authority: pki/cluster-ca.pem
server: https://10.0.0.1:6553
name: kubernetes_target
contexts:
- context:
cluster: kubernetes_target
user: admin
name: admin@kubernetes
current-context: admin@kubernetes
kind: Config
preferences: {}
users:
- name: admin
user:
client-certificate: pki/admin.pem
client-key: pki/admin-key.pem

View File

@ -0,0 +1,76 @@
/*
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 (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"opendev.org/airship/airshipui/internal/configs"
)
func TestSendAlert(t *testing.T) {
client, err := NewTestClient()
require.NoError(t, err)
defer client.Close()
// construct and send alert from server to client
SendAlert(configs.Error, "Test Alert", true)
var response configs.WsMessage
err = client.ReadJSON(&response)
require.NoError(t, err)
expected := configs.WsMessage{
Type: configs.Alert,
Component: configs.Error,
Message: "Test Alert",
Fade: true,
// don't fail on timestamp diff
Timestamp: response.Timestamp,
}
assert.Equal(t, expected, response)
}
func TestSendAlertNoWebSocket(t *testing.T) {
// test requires that ws == nil
conn := ws
ws = nil
defer func() {
ws = conn
Alerts = nil
}()
// queue should be empty
Alerts = nil
SendAlert(configs.Info, "Test Alert", true)
// ws is nil, so the queue should now have 1 Alert
assert.Len(t, Alerts, 1)
expected := configs.WsMessage{
Type: configs.Alert,
Component: configs.Info,
Message: "Test Alert",
Fade: true,
// don't fail on timestamp diff
Timestamp: Alerts[0].Timestamp,
}
assert.Equal(t, expected, Alerts[0])
}

View File

@ -187,7 +187,7 @@ func WebServer() {
func clientInit(configs.WsMessage) configs.WsMessage {
// if no auth method is supplied start with minimal functionality
if len(configs.UIConfig.AuthMethod.URL) == 0 {
if configs.UIConfig.AuthMethod == nil {
isAuthenticated = true
}

View File

@ -0,0 +1,303 @@
/*
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"
"io/ioutil"
"net/http"
"net/url"
"testing"
"time"
"opendev.org/airship/airshipui/internal/configs"
"opendev.org/airship/airshipui/testutil"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
serverAddr string = "localhost:8080"
testBaremetalHTML string = "../integrations/ctl/testdata/baremetal.html"
testDocumentHTML string = "../integrations/ctl/testdata/document.html"
// client messages
initialize string = `{"type":"electron","component":"initialize"}`
keepalive string = `{"type":"electron","component":"keepalive"}`
unknownType string = `{"type":"fake_type","component":"initialize"}`
unknownComponent string = `{"type":"electron","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"}`
)
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)
defer client.Close()
isAuthenticated = false
// trigger web server's handleAuth function
_, err = http.Get("http://localhost:8080/auth")
require.NoError(t, err)
var response configs.WsMessage
err = client.ReadJSON(&response)
require.NoError(t, err)
expected := configs.WsMessage{
Type: configs.Electron,
Component: configs.Authcomplete,
// don't fail on timestamp diff
Timestamp: response.Timestamp,
}
// isAuthenticated should now be true after auth complete
assert.Equal(t, isAuthenticated, true)
assert.Equal(t, expected, response)
}
func TestHandleDocumentRequest(t *testing.T) {
client, err := NewTestClient()
require.NoError(t, err)
defer client.Close()
expectedHTML, err := ioutil.ReadFile(testDocumentHTML)
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: string(expectedHTML),
// don't fail on timestamp diff
Timestamp: response.Timestamp,
}
assert.Equal(t, expected, response)
}
func TestHandleBaremetalRequest(t *testing.T) {
client, err := NewTestClient()
require.NoError(t, err)
defer client.Close()
expectedHTML, err := ioutil.ReadFile(testBaremetalHTML)
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: string(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
u := url.URL{Scheme: "ws", Host: serverAddr, Path: "/ws"}
// allow multiple attempts to establish websocket in case server isn't ready
for i := 0; i < 5; i++ {
client, _, err = websocket.DefaultDialer.Dial(u.String(), nil)
if err == nil {
return client, nil
}
time.Sleep(2 * time.Second)
}
return nil, err
}

131
testutil/testconfig.go Normal file
View File

@ -0,0 +1,131 @@
/*
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 testutil
import "opendev.org/airship/airshipui/internal/configs"
// DummyDashboardConfig returns a populated Dashboard struct
func DummyDashboardConfig() configs.Dashboard {
return configs.Dashboard{
Name: "dummy_dashboard",
Protocol: "http",
Hostname: "dummyhost",
Port: 80,
Path: "fake/login/path",
}
}
// DummyPluginDashboardConfig returns a populated PluginDashboard struct
func DummyPluginDashboardConfig() configs.PluginDashboard {
return configs.PluginDashboard{
Protocol: "http",
FQDN: "localhost",
Port: 80,
Path: "index.html",
}
}
// DummyExecutableConfig returns a populated Executable struct
func DummyExecutableConfig() configs.Executable {
return configs.Executable{
AutoStart: true,
Filepath: "/fake/path/to/executable",
Args: []string{
"--fakeflag",
"fakevalue",
},
}
}
// DummyAuthMethodConfig returns a populated AuthMethod struct
func DummyAuthMethodConfig() *configs.AuthMethod {
return &configs.AuthMethod{
URL: "http://fake.auth.method.com/auth",
}
}
// 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()
return configs.Config{
Plugins: []configs.Plugin{p, pn},
Clusters: []configs.Cluster{c},
}
}
// 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},
}
}

View File

@ -60,7 +60,7 @@ function addServiceDashboards(json) { // eslint-disable-line no-unused-vars
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.fqdn !== undefined) {
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);

6
web/package-lock.json generated
View File

@ -657,9 +657,9 @@
}
},
"entities": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.2.tgz",
"integrity": "sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw==",
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
"integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==",
"dev": true
},
"env-paths": {