From 7548c4bb9cf1201bd372f07091468f1da0e3a38f Mon Sep 17 00:00:00 2001 From: Matthew Fuller Date: Thu, 11 Jun 2020 15:59:45 -0700 Subject: [PATCH] Add testing to airshipui 1. Change embedded structs to pointers so json omitempty will behave as expected and omit empty structs from ws messages 2. Add dummy airshipui.json config file for testing 3. Add testutil package to build dummy config objects 4. Add tests for configs package 5. Add tests for webservice package 6. Add tests for integrations/ctl package Change-Id: I1a2ac543898cbbae96c764a983e7e9b73946a9d9 --- go.mod | 2 + internal/commands/root.go | 19 +- internal/configs/configs.go | 88 ++--- internal/configs/configs_test.go | 51 +++ internal/configs/testdata/airshipui.json | 55 +++ internal/integrations/ctl/airshipctl.go | 9 + internal/integrations/ctl/baremetal.go | 3 +- internal/integrations/ctl/baremetal_test.go | 69 ++++ internal/integrations/ctl/config.go | 9 +- internal/integrations/ctl/config_test.go | 88 +++++ internal/integrations/ctl/document.go | 3 +- internal/integrations/ctl/document_test.go | 69 ++++ .../integrations/ctl/templates/baremetal.html | 10 +- .../integrations/ctl/templates/config.html | 322 +++++++++--------- .../integrations/ctl/templates/document.html | 10 +- .../integrations/ctl/testdata/baremetal.html | 5 + .../integrations/ctl/testdata/config.html | 160 +++++++++ .../integrations/ctl/testdata/config.yaml | 73 ++++ .../integrations/ctl/testdata/document.html | 5 + .../integrations/ctl/testdata/kubeconfig.yaml | 19 ++ internal/webservice/alerts_test.go | 76 +++++ internal/webservice/server.go | 2 +- internal/webservice/server_test.go | 303 ++++++++++++++++ testutil/testconfig.go | 131 +++++++ web/js/common.js | 2 +- web/package-lock.json | 6 +- 26 files changed, 1365 insertions(+), 224 deletions(-) create mode 100644 internal/configs/configs_test.go create mode 100644 internal/configs/testdata/airshipui.json create mode 100644 internal/integrations/ctl/baremetal_test.go create mode 100644 internal/integrations/ctl/config_test.go create mode 100644 internal/integrations/ctl/document_test.go create mode 100644 internal/integrations/ctl/testdata/baremetal.html create mode 100644 internal/integrations/ctl/testdata/config.html create mode 100644 internal/integrations/ctl/testdata/config.yaml create mode 100644 internal/integrations/ctl/testdata/document.html create mode 100644 internal/integrations/ctl/testdata/kubeconfig.yaml create mode 100644 internal/webservice/alerts_test.go create mode 100644 internal/webservice/server_test.go create mode 100644 testutil/testconfig.go diff --git a/go.mod b/go.mod index 754d9b2..545568e 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/internal/commands/root.go b/internal/commands/root.go index 6caa540..3cda458 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -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 +} diff --git a/internal/configs/configs.go b/internal/configs/configs.go index fc1ada6..43c554c 100755 --- a/internal/configs/configs.go +++ b/internal/configs/configs.go @@ -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 } diff --git a/internal/configs/configs_test.go b/internal/configs/configs_test.go new file mode 100644 index 0000000..0736ee8 --- /dev/null +++ b/internal/configs/configs_test.go @@ -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) +} diff --git a/internal/configs/testdata/airshipui.json b/internal/configs/testdata/airshipui.json new file mode 100644 index 0000000..02d727f --- /dev/null +++ b/internal/configs/testdata/airshipui.json @@ -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" + } + ] + } + ] + } + ] +} diff --git a/internal/integrations/ctl/airshipctl.go b/internal/integrations/ctl/airshipctl.go index f848ebe..1f38217 100755 --- a/internal/integrations/ctl/airshipctl.go +++ b/internal/integrations/ctl/airshipctl.go @@ -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) diff --git a/internal/integrations/ctl/baremetal.go b/internal/integrations/ctl/baremetal.go index 6dbc564..c19073b 100755 --- a/internal/integrations/ctl/baremetal.go +++ b/internal/integrations/ctl/baremetal.go @@ -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) } diff --git a/internal/integrations/ctl/baremetal_test.go b/internal/integrations/ctl/baremetal_test.go new file mode 100644 index 0000000..9d5a509 --- /dev/null +++ b/internal/integrations/ctl/baremetal_test.go @@ -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) +} diff --git a/internal/integrations/ctl/config.go b/internal/integrations/ctl/config.go index c3bc0c5..2c49025 100755 --- a/internal/integrations/ctl/config.go +++ b/internal/integrations/ctl/config.go @@ -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 { diff --git a/internal/integrations/ctl/config_test.go b/internal/integrations/ctl/config_test.go new file mode 100644 index 0000000..2798a7b --- /dev/null +++ b/internal/integrations/ctl/config_test.go @@ -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) +} diff --git a/internal/integrations/ctl/document.go b/internal/integrations/ctl/document.go index 98aec96..881cb26 100755 --- a/internal/integrations/ctl/document.go +++ b/internal/integrations/ctl/document.go @@ -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(), }) diff --git a/internal/integrations/ctl/document_test.go b/internal/integrations/ctl/document_test.go new file mode 100644 index 0000000..3f4a779 --- /dev/null +++ b/internal/integrations/ctl/document_test.go @@ -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) +} diff --git a/internal/integrations/ctl/templates/baremetal.html b/internal/integrations/ctl/templates/baremetal.html index 71c4c33..4f7db6c 100755 --- a/internal/integrations/ctl/templates/baremetal.html +++ b/internal/integrations/ctl/templates/baremetal.html @@ -1,5 +1,5 @@ -

Airship CTL {{.Title}} Base Information

-

Version: {{.Version}}

- -

Generate ISO

- +

Airship CTL {{.Title}} Base Information

+

Version: {{.Version}}

+ +

Generate ISO

+ diff --git a/internal/integrations/ctl/templates/config.html b/internal/integrations/ctl/templates/config.html index e6f56d4..05fe6f3 100755 --- a/internal/integrations/ctl/templates/config.html +++ b/internal/integrations/ctl/templates/config.html @@ -1,162 +1,160 @@ -

Airship CTL {{.Title}} Base Information

-

Version: {{.Version}}

- - - -
-

- - - - - - - - - - - - - - {{.ClusterRows}} - -
Bootstrap InfoCluster Kube ConfManagement ConfigurationLocation Of OriginServerCertificate Authority
- -

-
- - - - - - -
-

- - - - - - - - - - - - - {{.ContextRows}} - -
Context Kube ConfManifestLocation Of OriginClusterUser
-

- -
- - - - - - -
-

- - - - - - - - - - {{.CredentialRows}} - -
Location Of OriginUsername
- -

-
- - - - - +

Airship CTL {{.Title}} Base Information

+

Version: {{.Version}}

+ + + +
+

+ + + + + + + + + + + + + + {{.ClusterRows}} + +
Bootstrap InfoCluster Kube ConfManagement ConfigurationLocation Of OriginServerCertificate Authority
+ +

+
+ + + + + + +
+

+ + + + + + + + + + + + + {{.ContextRows}} + +
Context Kube ConfManifestLocation Of OriginClusterUser
+

+ +
+ + + + + + +
+

+ + + + + + + + + + {{.CredentialRows}} + +
Location Of OriginUsername
+ +

+
+ + + diff --git a/internal/integrations/ctl/templates/document.html b/internal/integrations/ctl/templates/document.html index 6345250..1d2d60b 100755 --- a/internal/integrations/ctl/templates/document.html +++ b/internal/integrations/ctl/templates/document.html @@ -1,5 +1,5 @@ -

Airship CTL {{.Title}} Base Information

-

Version: {{.Version}}

- -

Document Pull

- +

Airship CTL {{.Title}} Base Information

+

Version: {{.Version}}

+ +

Document Pull

+ diff --git a/internal/integrations/ctl/testdata/baremetal.html b/internal/integrations/ctl/testdata/baremetal.html new file mode 100644 index 0000000..cd08bdd --- /dev/null +++ b/internal/integrations/ctl/testdata/baremetal.html @@ -0,0 +1,5 @@ +

Airship CTL Baremetal Base Information

+

Version: devel

+ +

Generate ISO

+ diff --git a/internal/integrations/ctl/testdata/config.html b/internal/integrations/ctl/testdata/config.html new file mode 100644 index 0000000..914ac31 --- /dev/null +++ b/internal/integrations/ctl/testdata/config.html @@ -0,0 +1,160 @@ +

Airship CTL Config Base Information

+

Version: devel

+ + + +
+

+ + + + + + + + + + + + + + + +
Bootstrap InfoCluster Kube ConfManagement ConfigurationLocation Of OriginServerCertificate Authority
default
kubernetes_target
default
testdata/kubeconfig.yaml
https://10.0.0.1:6553
pki/cluster-ca.pem
+ +

+
+ + + + + + +
+

+ + + + + + + + + + + + + + +
Context Kube ConfManifestLocation Of OriginClusterUser
kubernetes_target
testdata/kubeconfig.yaml
kubernetes_target
admin
+

+ +
+ + + + + + +
+

+ + + + + + + + + + + +
Location Of OriginUsername
+ +

+
+ + + diff --git a/internal/integrations/ctl/testdata/config.yaml b/internal/integrations/ctl/testdata/config.yaml new file mode 100644 index 0000000..41f85a6 --- /dev/null +++ b/internal/integrations/ctl/testdata/config.yaml @@ -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: {} diff --git a/internal/integrations/ctl/testdata/document.html b/internal/integrations/ctl/testdata/document.html new file mode 100644 index 0000000..a8282e8 --- /dev/null +++ b/internal/integrations/ctl/testdata/document.html @@ -0,0 +1,5 @@ +

Airship CTL Document Base Information

+

Version: devel

+ +

Document Pull

+ diff --git a/internal/integrations/ctl/testdata/kubeconfig.yaml b/internal/integrations/ctl/testdata/kubeconfig.yaml new file mode 100644 index 0000000..20428d8 --- /dev/null +++ b/internal/integrations/ctl/testdata/kubeconfig.yaml @@ -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 diff --git a/internal/webservice/alerts_test.go b/internal/webservice/alerts_test.go new file mode 100644 index 0000000..4968793 --- /dev/null +++ b/internal/webservice/alerts_test.go @@ -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]) +} diff --git a/internal/webservice/server.go b/internal/webservice/server.go index d17c20b..cec3837 100755 --- a/internal/webservice/server.go +++ b/internal/webservice/server.go @@ -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 } diff --git a/internal/webservice/server_test.go b/internal/webservice/server_test.go new file mode 100644 index 0000000..89036f7 --- /dev/null +++ b/internal/webservice/server_test.go @@ -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 +} diff --git a/testutil/testconfig.go b/testutil/testconfig.go new file mode 100644 index 0000000..f56ccaf --- /dev/null +++ b/testutil/testconfig.go @@ -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}, + } +} diff --git a/web/js/common.js b/web/js/common.js index 909db98..312032b 100755 --- a/web/js/common.js +++ b/web/js/common.js @@ -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); diff --git a/web/package-lock.json b/web/package-lock.json index 751fe2d..99f968b 100755 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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": {