@mashroom/mashroom-remote-package-scanner-kubernetes 3.0.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/dist/context.js +72 -0
- package/dist/health/health-probe.js +51 -0
- package/dist/k8s/DummyKubernetesConnector.js +114 -0
- package/dist/k8s/KubernetesConnector.js +52 -0
- package/dist/k8s/WatchWithReconnect.js +46 -0
- package/dist/metrics/metrics.js +32 -0
- package/dist/scanner/KubernetesRemotePluginPackagesScanner.js +127 -0
- package/dist/scanner/mashroom-bootstrap-plugin-package-scanner.js +42 -0
- package/dist/scanner/podEventCallback.js +62 -0
- package/dist/scanner/removePluginPackageService.js +16 -0
- package/dist/scanner/serviceEventCallback.js +76 -0
- package/dist/scanner/updatePluginPackageService.js +55 -0
- package/dist/types.js +5 -0
- package/dist/webapp/dev-server.js +103 -0
- package/dist/webapp/mashroom-bootstrap-webapp.js +12 -0
- package/dist/webapp/routes/services.js +61 -0
- package/dist/webapp/webapp.js +20 -0
- package/mashroom.json +39 -0
- package/package.json +53 -0
- package/public/style.css +1 -0
- package/public/style.css.map +1 -0
- package/views/services.handlebars +100 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) nonblocking.at gmbh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
|
|
2
|
+
# Mashroom Remote Package Scanner Kubernetes
|
|
3
|
+
|
|
4
|
+
Plugin for [Mashroom Server](https://www.mashroom-server.com), a **Microfrontend Integration Platform**.
|
|
5
|
+
|
|
6
|
+
This plugin scans for remote plugin packages in a Kubernetes cluster.
|
|
7
|
+
It *watches* Kubernetes namespaces and services with given labels which will be checked for plugins.
|
|
8
|
+
|
|
9
|
+
It also watches Pods for new image versions and removes plugin packages if no running Pod can be found.
|
|
10
|
+
|
|
11
|
+
> [!IMPORTANT]
|
|
12
|
+
> A remote plugin package **must** expose a definition file, typically */mashroom.json*.
|
|
13
|
+
> You can use a *mashroom-server.com/remote-plugins-definition-path* annotation on the K8S service to tell Mashroom where to find it.
|
|
14
|
+
|
|
15
|
+
Currently, only the following plugin types are known to be supported:
|
|
16
|
+
|
|
17
|
+
* portal-app
|
|
18
|
+
* portal-app2
|
|
19
|
+
* portal-layouts
|
|
20
|
+
* portal-page-enhancement (only if no bootstrap is defined)
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
If *node_modules/@mashroom* is configured as a plugin path, add **@mashroom/mashroom-remote-package-scanner-kubernetes** as *dependency*.
|
|
25
|
+
|
|
26
|
+
You can override the default config in your Mashroom server config like this:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"plugins": {
|
|
31
|
+
"Mashroom Remote Package Scanner Kubernetes": {
|
|
32
|
+
"k8sNamespacesLabelSelector": ["microfrontends=true"],
|
|
33
|
+
"k8sNamespaces": null,
|
|
34
|
+
"k8sServiceLabelSelector": ["microfrontends=true"],
|
|
35
|
+
"serviceNameFilter": ".*"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
* _k8sNamespacesLabelSelector_: Label selector(s) for namespaces, can be a single string or an array, can also be null (Default: microfrontends=true)
|
|
42
|
+
* _k8sNamespaces_: A distinct list of Kubernetes namespaces to check, can be null if _k8sNamespacesLabelSelector_ is set (Default: null)
|
|
43
|
+
* _k8sServiceLabelSelector_: Label selector(s) for services, can be a single string or an array, can be null (Default: microfrontends=true)
|
|
44
|
+
* _serviceNameFilter_: A regular expression for services that should be checked (case-insensitive). (Default: ".*")
|
|
45
|
+
|
|
46
|
+
**A more complex example**
|
|
47
|
+
|
|
48
|
+
Select all services with the label *microfrontend=true* and not label *channel=alpha* in all namespaces with label *environment=development* and *tier=frontend*:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"plugins": {
|
|
53
|
+
"Mashroom Portal Remote App Kubernetes Background Job": {
|
|
54
|
+
"k8sNamespacesLabelSelector": ["environment=development,tier=frontend"],
|
|
55
|
+
"k8sServiceLabelSelector": ["microfrontend=true,channel!=alpha"]
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Annotations
|
|
62
|
+
|
|
63
|
+
You can use annotations on Kubernetes services to help Mashroom to understand the setup:
|
|
64
|
+
|
|
65
|
+
| Annotation | Description | Default |
|
|
66
|
+
|----------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|
|
|
67
|
+
| mashroom-server.com/remote-plugins-definition-path | The path of the plugins definition | /mashroom.json, /mashroom.yaml |
|
|
68
|
+
| open-microfrontends.org/microfrontends | The server actually contains OpenMicrofrontend compliant Apps, requires [mashroom-portal-open-microfrontends-support](../mashroom-portal-open-microfrontends-support) | |
|
|
69
|
+
| open-microfrontends.org/description-path | The path of the OpenMicrofrontends description | /microfrontends.yaml |
|
|
70
|
+
|
|
71
|
+
## Setup Kubernetes access
|
|
72
|
+
|
|
73
|
+
This plugin uses the Kubernetes API and requires the following permissions:
|
|
74
|
+
|
|
75
|
+
* **watch** for namespaces cluster-wide
|
|
76
|
+
* **watch** for services in all namespaces with plugins (or cluster-wide)
|
|
77
|
+
* **watch** for pods in all namespaces with plugins (or cluster-wide)
|
|
78
|
+
|
|
79
|
+
To allow this plugin to use the Kubernetes API you need to attach a Kubernetes **Service Account** with the correct permissions to the deployment.
|
|
80
|
+
|
|
81
|
+
Here is a possible setup with cluster-wide permissions:
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
85
|
+
kind: ClusterRole
|
|
86
|
+
metadata:
|
|
87
|
+
name: list-namespaces-services-cluster-role
|
|
88
|
+
rules:
|
|
89
|
+
- apiGroups:
|
|
90
|
+
- ""
|
|
91
|
+
resources:
|
|
92
|
+
- services
|
|
93
|
+
- pods
|
|
94
|
+
- namespaces
|
|
95
|
+
verbs:
|
|
96
|
+
- watch
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
apiVersion: v1
|
|
101
|
+
kind: ServiceAccount
|
|
102
|
+
metadata:
|
|
103
|
+
name: mashroom-portal
|
|
104
|
+
namespace: default
|
|
105
|
+
---
|
|
106
|
+
apiVersion: rbac.authorization.k8s.io/v1
|
|
107
|
+
kind: ClusterRoleBinding
|
|
108
|
+
metadata:
|
|
109
|
+
name: mashroom-portal-role-binding
|
|
110
|
+
subjects:
|
|
111
|
+
- kind: ServiceAccount
|
|
112
|
+
name: mashroom-portal
|
|
113
|
+
namespace: default
|
|
114
|
+
roleRef:
|
|
115
|
+
kind: ClusterRole
|
|
116
|
+
name: list-namespaces-services-cluster-role
|
|
117
|
+
apiGroup: rbac.authorization.k8s.io
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
And in your deployment resource state the Service Account name:
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
apiVersion: apps/v1
|
|
124
|
+
kind: Deployment
|
|
125
|
+
metadata:
|
|
126
|
+
name: mashroom-portal
|
|
127
|
+
namespace: default
|
|
128
|
+
spec:
|
|
129
|
+
# ...
|
|
130
|
+
template:
|
|
131
|
+
# ...
|
|
132
|
+
spec:
|
|
133
|
+
containers:
|
|
134
|
+
- name: mashroom-portal
|
|
135
|
+
# ...
|
|
136
|
+
serviceAccountName: mashroom-portal
|
|
137
|
+
```
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
let _namespaceLabelSelector = null;
|
|
8
|
+
let _serviceLabelSelector = null;
|
|
9
|
+
let _serviceNameFilter = '';
|
|
10
|
+
let _watchedNamespaces = [];
|
|
11
|
+
let _services = [];
|
|
12
|
+
let _runningPods = [];
|
|
13
|
+
let _errors = [];
|
|
14
|
+
let _scannerCallback = null;
|
|
15
|
+
let _initialScanDone = false;
|
|
16
|
+
const context = {
|
|
17
|
+
get namespaceLabelSelector() {
|
|
18
|
+
return _namespaceLabelSelector;
|
|
19
|
+
},
|
|
20
|
+
set namespaceLabelSelector(namespaceLabelSelector) {
|
|
21
|
+
_namespaceLabelSelector = namespaceLabelSelector;
|
|
22
|
+
},
|
|
23
|
+
get serviceLabelSelector() {
|
|
24
|
+
return _serviceLabelSelector;
|
|
25
|
+
},
|
|
26
|
+
set serviceLabelSelector(serviceLabelSelector) {
|
|
27
|
+
_serviceLabelSelector = serviceLabelSelector;
|
|
28
|
+
},
|
|
29
|
+
get serviceNameFilter() {
|
|
30
|
+
return _serviceNameFilter;
|
|
31
|
+
},
|
|
32
|
+
set serviceNameFilter(serviceNameFilter) {
|
|
33
|
+
_serviceNameFilter = serviceNameFilter;
|
|
34
|
+
},
|
|
35
|
+
get watchedNamespaces() {
|
|
36
|
+
return _watchedNamespaces;
|
|
37
|
+
},
|
|
38
|
+
set watchedNamespaces(watchedNamespaces) {
|
|
39
|
+
_watchedNamespaces = watchedNamespaces;
|
|
40
|
+
},
|
|
41
|
+
get services() {
|
|
42
|
+
return _services;
|
|
43
|
+
},
|
|
44
|
+
set services(services) {
|
|
45
|
+
_services = services;
|
|
46
|
+
},
|
|
47
|
+
get runningPods() {
|
|
48
|
+
return _runningPods;
|
|
49
|
+
},
|
|
50
|
+
set runningPods(pods) {
|
|
51
|
+
_runningPods = pods;
|
|
52
|
+
},
|
|
53
|
+
get errors() {
|
|
54
|
+
return _errors;
|
|
55
|
+
},
|
|
56
|
+
set errors(errors) {
|
|
57
|
+
_errors = errors;
|
|
58
|
+
},
|
|
59
|
+
get scannerCallback() {
|
|
60
|
+
return _scannerCallback;
|
|
61
|
+
},
|
|
62
|
+
set scannerCallback(scannerCallback) {
|
|
63
|
+
_scannerCallback = scannerCallback;
|
|
64
|
+
},
|
|
65
|
+
get initialScanDone() {
|
|
66
|
+
return _initialScanDone;
|
|
67
|
+
},
|
|
68
|
+
set initialScanDone(done) {
|
|
69
|
+
_initialScanDone = done;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
var _default = exports.default = context;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _context = _interopRequireDefault(require("../context"));
|
|
8
|
+
var _KubernetesRemotePluginPackagesScanner = require("../scanner/KubernetesRemotePluginPackagesScanner");
|
|
9
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
let oneFullScanDone = false;
|
|
11
|
+
var _default = pluginContextHolder => ({
|
|
12
|
+
async check() {
|
|
13
|
+
if (!oneFullScanDone) {
|
|
14
|
+
const pluginContext = pluginContextHolder.getPluginContext();
|
|
15
|
+
const logger = pluginContext.loggerFactory('mashroom.scanner.remotePackageKubernetes');
|
|
16
|
+
const pluginService = pluginContext.services.core.pluginService;
|
|
17
|
+
const services = _context.default.services;
|
|
18
|
+
const pluginPackages = pluginService.getPotentialPluginPackagesByScanner(_KubernetesRemotePluginPackagesScanner.SCANNER_NAME);
|
|
19
|
+
let done = true;
|
|
20
|
+
if (!_context.default.initialScanDone) {
|
|
21
|
+
done = false;
|
|
22
|
+
} else {
|
|
23
|
+
for (const service of services) {
|
|
24
|
+
const pluginPackage = pluginPackages.find(p => service.url && p.url.toString() === service.url.toString());
|
|
25
|
+
if (pluginPackage) {
|
|
26
|
+
if (!pluginPackage.processedOnce) {
|
|
27
|
+
done = false;
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
logger.warn(`Found no registered plugin package for Kubernetes service: ${service.url}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (done) {
|
|
36
|
+
oneFullScanDone = true;
|
|
37
|
+
return {
|
|
38
|
+
ready: true
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
ready: false,
|
|
43
|
+
error: 'Initial Kubernetes plugin packages scan in progress'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
ready: true
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
exports.default = _default;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
class KubernetesConnector {
|
|
8
|
+
async watchNamespaces(labelSelector, cb) {
|
|
9
|
+
if (labelSelector === 'environment=development') {
|
|
10
|
+
cb('ADDED', {
|
|
11
|
+
metadata: {
|
|
12
|
+
name: 'dev-namespace1',
|
|
13
|
+
labels: {
|
|
14
|
+
environment: 'development'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
cb('ADDED', {
|
|
19
|
+
metadata: {
|
|
20
|
+
name: 'dev-namespace2',
|
|
21
|
+
labels: {
|
|
22
|
+
environment: 'development'
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
} else if (labelSelector === 'environment=development2') {
|
|
27
|
+
cb('ADDED', {
|
|
28
|
+
metadata: {
|
|
29
|
+
name: 'dev-namespace3',
|
|
30
|
+
labels: {
|
|
31
|
+
environment: 'dev2'
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
cb('ADDED', {
|
|
37
|
+
metadata: {
|
|
38
|
+
name: 'whata-namespace'
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return new AbortController();
|
|
42
|
+
}
|
|
43
|
+
async watchServices(namespace, labelSelector, cb) {
|
|
44
|
+
if (namespace === 'dev-namespace1') {
|
|
45
|
+
throw new Error('Permission denied');
|
|
46
|
+
} else if (namespace === 'dev-namespace2' || labelSelector === 'environment=dev') {
|
|
47
|
+
cb('ADDED', {
|
|
48
|
+
metadata: {
|
|
49
|
+
name: 'my-remote-app',
|
|
50
|
+
namespace: 'dev-namespace2'
|
|
51
|
+
},
|
|
52
|
+
spec: {
|
|
53
|
+
clusterIP: '127.0.0.1',
|
|
54
|
+
ports: [{
|
|
55
|
+
port: 6066
|
|
56
|
+
}]
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
} else if (namespace === 'dev-namespace3') {
|
|
60
|
+
cb('ADDED', {
|
|
61
|
+
metadata: {
|
|
62
|
+
name: 'my-remote-app',
|
|
63
|
+
namespace: 'dev-namespace3'
|
|
64
|
+
},
|
|
65
|
+
spec: {
|
|
66
|
+
clusterIP: '127.0.0.1',
|
|
67
|
+
ports: [{
|
|
68
|
+
port: 6066
|
|
69
|
+
}]
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
cb('ADDED', {
|
|
74
|
+
metadata: {
|
|
75
|
+
name: 'mashroom-demo-remote-portal-app',
|
|
76
|
+
namespace: 'default'
|
|
77
|
+
},
|
|
78
|
+
spec: {
|
|
79
|
+
clusterIP: '127.0.0.1',
|
|
80
|
+
ports: [{
|
|
81
|
+
port: 6088
|
|
82
|
+
}]
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
cb('ADDED', {
|
|
86
|
+
metadata: {
|
|
87
|
+
name: 'unreachable-service',
|
|
88
|
+
namespace: 'default'
|
|
89
|
+
},
|
|
90
|
+
spec: {
|
|
91
|
+
clusterIP: '1.2.3.4',
|
|
92
|
+
ports: [{
|
|
93
|
+
port: 8080
|
|
94
|
+
}]
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
cb('ADDED', {
|
|
98
|
+
metadata: {
|
|
99
|
+
name: 'headless-service',
|
|
100
|
+
namespace: 'default'
|
|
101
|
+
},
|
|
102
|
+
spec: {
|
|
103
|
+
ports: [{
|
|
104
|
+
port: 8080
|
|
105
|
+
}]
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return new AbortController();
|
|
109
|
+
}
|
|
110
|
+
async watchPods(namespace, cb) {
|
|
111
|
+
return new AbortController();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.default = KubernetesConnector;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _WatchWithReconnect = _interopRequireDefault(require("./WatchWithReconnect"));
|
|
8
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
class KubernetesConnector {
|
|
10
|
+
constructor(localConfig, loggerFactory) {
|
|
11
|
+
this.localConfig = localConfig;
|
|
12
|
+
this._logger = loggerFactory('mashroom.scanner.remotePackageKubernetes');
|
|
13
|
+
}
|
|
14
|
+
async watchNamespaces(labelSelector, cb) {
|
|
15
|
+
if (!this._watch) {
|
|
16
|
+
await this.#init();
|
|
17
|
+
}
|
|
18
|
+
return this._watch.watch('/api/v1/namespaces', {
|
|
19
|
+
labelSelector
|
|
20
|
+
}, cb);
|
|
21
|
+
}
|
|
22
|
+
async watchServices(namespace, labelSelector, cb) {
|
|
23
|
+
if (!this._watch) {
|
|
24
|
+
await this.#init();
|
|
25
|
+
}
|
|
26
|
+
return this._watch.watch(`/api/v1/namespaces/${namespace}/services`, {
|
|
27
|
+
labelSelector
|
|
28
|
+
}, cb);
|
|
29
|
+
}
|
|
30
|
+
async watchPods(namespace, cb) {
|
|
31
|
+
if (!this._watch) {
|
|
32
|
+
await this.#init();
|
|
33
|
+
}
|
|
34
|
+
return this._watch.watch(`/api/v1/namespaces/${namespace}/pods`, {}, cb);
|
|
35
|
+
}
|
|
36
|
+
async #init() {
|
|
37
|
+
// '@kubernetes/client-node' is an ESM only module
|
|
38
|
+
const {
|
|
39
|
+
KubeConfig
|
|
40
|
+
} = await import('@kubernetes/client-node');
|
|
41
|
+
const k8sClient = new KubeConfig();
|
|
42
|
+
if (!this.localConfig) {
|
|
43
|
+
// This only works if the Portal runs within a Kubernetes Pod with a valid service account attached
|
|
44
|
+
k8sClient.loadFromCluster();
|
|
45
|
+
} else {
|
|
46
|
+
// This uses the local kubectl config (only for test purposes!)
|
|
47
|
+
k8sClient.loadFromDefault();
|
|
48
|
+
}
|
|
49
|
+
this._watch = new _WatchWithReconnect.default(k8sClient, this._logger);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
exports.default = KubernetesConnector;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _clientNode = require("@kubernetes/client-node");
|
|
8
|
+
class WatchWithReconnect {
|
|
9
|
+
#watch;
|
|
10
|
+
constructor(k8sClient, logger) {
|
|
11
|
+
this.logger = logger;
|
|
12
|
+
this.#watch = new _clientNode.Watch(k8sClient);
|
|
13
|
+
}
|
|
14
|
+
async watch(path, queryParams, cb) {
|
|
15
|
+
let aborted = false;
|
|
16
|
+
let controller;
|
|
17
|
+
const startWatcher = async () => {
|
|
18
|
+
this.logger.info('Adding K8S watcher for path: ', path);
|
|
19
|
+
controller = await this.#watch.watch(path, queryParams, (phase, apiObj) => {
|
|
20
|
+
cb(phase, apiObj);
|
|
21
|
+
}, error => {
|
|
22
|
+
if (!error) {
|
|
23
|
+
this.logger.info(`K8S Watcher on ${path} lost connection`);
|
|
24
|
+
} else {
|
|
25
|
+
this.logger.error(`K8S Watcher error on ${path}:`, error);
|
|
26
|
+
}
|
|
27
|
+
if (!aborted) {
|
|
28
|
+
this.logger.info(`Reconnecting K8S Watcher on ${path}...`);
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
startWatcher();
|
|
31
|
+
}, 1000);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
await startWatcher();
|
|
36
|
+
return {
|
|
37
|
+
abort() {
|
|
38
|
+
aborted = true;
|
|
39
|
+
if (controller) {
|
|
40
|
+
controller.abort();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.default = WatchWithReconnect;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.unregisterKubernetesRemotePluginPackagesMetrics = exports.registerKubernetesRemotePluginPackagesMetrics = void 0;
|
|
7
|
+
var _context = _interopRequireDefault(require("../context"));
|
|
8
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
let callbackRef;
|
|
10
|
+
const registerKubernetesRemotePluginPackagesMetrics = pluginContextHolder => {
|
|
11
|
+
const register = async () => {
|
|
12
|
+
const pluginContext = pluginContextHolder.getPluginContext();
|
|
13
|
+
const collectorService = pluginContext.services.metrics?.service;
|
|
14
|
+
if (collectorService) {
|
|
15
|
+
callbackRef = await collectorService.addObservableCallback(asyncCollectorService => {
|
|
16
|
+
const services = _context.default.services;
|
|
17
|
+
const servicesTotal = services.length;
|
|
18
|
+
asyncCollectorService.gauge('mashroom_k8s_remote_plugin_packages_total', 'Mashroom Kubernetes Remote Plugin Packages Total').set(servicesTotal);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
// Wait a few seconds until collectorService is available
|
|
23
|
+
setTimeout(register, 5000);
|
|
24
|
+
};
|
|
25
|
+
exports.registerKubernetesRemotePluginPackagesMetrics = registerKubernetesRemotePluginPackagesMetrics;
|
|
26
|
+
const unregisterKubernetesRemotePluginPackagesMetrics = () => {
|
|
27
|
+
if (callbackRef) {
|
|
28
|
+
callbackRef.removeCallback();
|
|
29
|
+
callbackRef = undefined;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
exports.unregisterKubernetesRemotePluginPackagesMetrics = unregisterKubernetesRemotePluginPackagesMetrics;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = exports.SCANNER_NAME = void 0;
|
|
7
|
+
var _context = _interopRequireDefault(require("../context"));
|
|
8
|
+
var _serviceEventCallback = _interopRequireDefault(require("./serviceEventCallback"));
|
|
9
|
+
var _podEventCallback = _interopRequireDefault(require("./podEventCallback"));
|
|
10
|
+
var _removePluginPackageService = _interopRequireDefault(require("./removePluginPackageService"));
|
|
11
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
const SCANNER_NAME = exports.SCANNER_NAME = 'Kubernetes Remote Portal Apps Plugin Scanner';
|
|
13
|
+
class KubernetesRemotePluginPackagesScanner {
|
|
14
|
+
_namespaceWatchers = [];
|
|
15
|
+
constructor(_namespaceLabelSelector, _namespaces, _serviceLabelSelector, serviceNameFilterStr, _kubernetesConnector, loggerFactory) {
|
|
16
|
+
this._namespaceLabelSelector = _namespaceLabelSelector;
|
|
17
|
+
this._namespaces = _namespaces;
|
|
18
|
+
this._serviceLabelSelector = _serviceLabelSelector;
|
|
19
|
+
this._kubernetesConnector = _kubernetesConnector;
|
|
20
|
+
this._serviceNameFilter = new RegExp(serviceNameFilterStr || '.*', 'i');
|
|
21
|
+
this._logger = loggerFactory('mashroom.scanner.remotePackageKubernetes');
|
|
22
|
+
}
|
|
23
|
+
get name() {
|
|
24
|
+
return SCANNER_NAME;
|
|
25
|
+
}
|
|
26
|
+
setCallback(callback) {
|
|
27
|
+
_context.default.scannerCallback = callback;
|
|
28
|
+
}
|
|
29
|
+
async start() {
|
|
30
|
+
if (Array.isArray(this._namespaces)) {
|
|
31
|
+
this._namespaces.forEach(ns => {
|
|
32
|
+
this._startWatchNamespace(ns);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (this._namespaceLabelSelector) {
|
|
36
|
+
const labelSelectors = Array.isArray(this._namespaceLabelSelector) ? this._namespaceLabelSelector : [this._namespaceLabelSelector];
|
|
37
|
+
for (const labelSelector of labelSelectors) {
|
|
38
|
+
try {
|
|
39
|
+
const abortController = await this._kubernetesConnector.watchNamespaces(labelSelector, this._namespaceEventCallback.bind(this));
|
|
40
|
+
this._namespaceWatchers.push(abortController);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
this._logger.error(`Watching Kubernetes Namespace with labelSelector ${labelSelector} failed!`, e);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Wait a few seconds until the initial services are added
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
_context.default.initialScanDone = true;
|
|
50
|
+
}, 3000);
|
|
51
|
+
}
|
|
52
|
+
async stop() {
|
|
53
|
+
// Abort all watches
|
|
54
|
+
for (const namespaceWatcher of this._namespaceWatchers) {
|
|
55
|
+
namespaceWatcher.abort();
|
|
56
|
+
}
|
|
57
|
+
for (const watchedNamespace of _context.default.watchedNamespaces) {
|
|
58
|
+
for (const watcher of watchedNamespace.servicesWatchers) {
|
|
59
|
+
watcher.abort();
|
|
60
|
+
}
|
|
61
|
+
for (const watcher of watchedNamespace.podsWatchers) {
|
|
62
|
+
watcher.abort();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async _startWatchNamespace(namespace) {
|
|
67
|
+
const existing = _context.default.watchedNamespaces.find(wn => wn.name === namespace);
|
|
68
|
+
if (existing) {
|
|
69
|
+
this._logger.debug(`Kubernetes Namespace ${namespace} already watched`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
this._logger.info(`Start watching Kubernetes Services and Pods in Namespace: ${namespace}`);
|
|
73
|
+
const servicesWatchers = [];
|
|
74
|
+
const podsWatchers = [];
|
|
75
|
+
_context.default.watchedNamespaces.push({
|
|
76
|
+
name: namespace,
|
|
77
|
+
servicesWatchers,
|
|
78
|
+
podsWatchers
|
|
79
|
+
});
|
|
80
|
+
let serviceLabelSelectors = [undefined];
|
|
81
|
+
if (this._serviceLabelSelector) {
|
|
82
|
+
serviceLabelSelectors = Array.isArray(this._serviceLabelSelector) ? this._serviceLabelSelector : [this._serviceLabelSelector];
|
|
83
|
+
}
|
|
84
|
+
for (const serviceLabelSelector of serviceLabelSelectors) {
|
|
85
|
+
try {
|
|
86
|
+
servicesWatchers.push(await this._kubernetesConnector.watchServices(namespace, serviceLabelSelector, (0, _serviceEventCallback.default)(this._serviceNameFilter, this._logger)));
|
|
87
|
+
} catch (e) {
|
|
88
|
+
this._logger.error(`Watching Kubernetes Services in Namespace ${namespace} with labelSelector ${serviceLabelSelector} failed!`, e);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
podsWatchers.push(await this._kubernetesConnector.watchPods(namespace, (0, _podEventCallback.default)(this._logger)));
|
|
93
|
+
} catch (e) {
|
|
94
|
+
this._logger.error(`Watching Kubernetes Pods in Namespace ${namespace} failed!`, e);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
_stopWatchNamespace(namespace) {
|
|
98
|
+
const existing = _context.default.watchedNamespaces.find(wn => wn.name === namespace);
|
|
99
|
+
if (!existing) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this._logger.info(`Stop watching and removing all remote plugin packages in Kubernetes Namespace: ${namespace}`);
|
|
103
|
+
for (const watcher of existing.servicesWatchers) {
|
|
104
|
+
watcher.abort();
|
|
105
|
+
}
|
|
106
|
+
for (const watcher of existing.podsWatchers) {
|
|
107
|
+
watcher.abort();
|
|
108
|
+
}
|
|
109
|
+
_context.default.watchedNamespaces = _context.default.watchedNamespaces.filter(wn => wn !== existing);
|
|
110
|
+
_context.default.runningPods = _context.default.runningPods.filter(pod => pod.namespace !== namespace);
|
|
111
|
+
const servicesInNamespace = _context.default.services.filter(service => service.namespace === namespace);
|
|
112
|
+
for (const sin of servicesInNamespace) {
|
|
113
|
+
(0, _removePluginPackageService.default)(sin, this._logger);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
_namespaceEventCallback(event, namespace) {
|
|
117
|
+
if (!namespace.metadata?.name) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (event === 'ADDED') {
|
|
121
|
+
this._startWatchNamespace(namespace.metadata.name);
|
|
122
|
+
} else if (event === 'DELETED') {
|
|
123
|
+
this._stopWatchNamespace(namespace.metadata.name);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.default = KubernetesRemotePluginPackagesScanner;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _metrics = require("../metrics/metrics");
|
|
8
|
+
var _healthProbe = _interopRequireDefault(require("../health/health-probe"));
|
|
9
|
+
var _DummyKubernetesConnector = _interopRequireDefault(require("../k8s/DummyKubernetesConnector"));
|
|
10
|
+
var _KubernetesConnector = _interopRequireDefault(require("../k8s/KubernetesConnector"));
|
|
11
|
+
var _context = _interopRequireDefault(require("../context"));
|
|
12
|
+
var _KubernetesRemotePluginPackagesScanner = _interopRequireDefault(require("./KubernetesRemotePluginPackagesScanner"));
|
|
13
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
14
|
+
const bootstrap = async (pluginName, pluginConfig, pluginContextHolder) => {
|
|
15
|
+
const {
|
|
16
|
+
services: {
|
|
17
|
+
core: {
|
|
18
|
+
pluginService,
|
|
19
|
+
healthProbeService
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
loggerFactory
|
|
23
|
+
} = pluginContextHolder.getPluginContext();
|
|
24
|
+
const {
|
|
25
|
+
namespaceLabelSelector,
|
|
26
|
+
namespaces,
|
|
27
|
+
serviceLabelSelector,
|
|
28
|
+
serviceNameFilter
|
|
29
|
+
} = pluginConfig;
|
|
30
|
+
_context.default.namespaceLabelSelector = namespaceLabelSelector;
|
|
31
|
+
_context.default.serviceLabelSelector = serviceLabelSelector;
|
|
32
|
+
_context.default.serviceNameFilter = serviceNameFilter;
|
|
33
|
+
healthProbeService.registerProbe(pluginName, (0, _healthProbe.default)(pluginContextHolder));
|
|
34
|
+
(0, _metrics.registerKubernetesRemotePluginPackagesMetrics)(pluginContextHolder);
|
|
35
|
+
pluginService.onUnloadOnce(pluginName, () => {
|
|
36
|
+
healthProbeService.unregisterProbe(pluginName);
|
|
37
|
+
(0, _metrics.unregisterKubernetesRemotePluginPackagesMetrics)();
|
|
38
|
+
});
|
|
39
|
+
const kubernetesConnector = process.env.K8S_CONNECTOR_DUMMY ? new _DummyKubernetesConnector.default() : process.env.K8S_CONNECTOR_LOCAL_CONFIG ? new _KubernetesConnector.default(true, loggerFactory) : new _KubernetesConnector.default(false, loggerFactory);
|
|
40
|
+
return new _KubernetesRemotePluginPackagesScanner.default(namespaceLabelSelector, namespaces, serviceLabelSelector, serviceNameFilter, kubernetesConnector, loggerFactory);
|
|
41
|
+
};
|
|
42
|
+
var _default = exports.default = bootstrap;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _context = _interopRequireDefault(require("../context"));
|
|
8
|
+
var _updatePluginPackageService = _interopRequireDefault(require("./updatePluginPackageService"));
|
|
9
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
var _default = logger => (event, pod) => {
|
|
11
|
+
if (!pod.metadata || !pod.metadata.uid) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// IMPORTANT: we consider only Pods which are running and where all containers are ready
|
|
16
|
+
if (pod.status?.phase !== 'Running' || pod.status?.containerStatuses?.find(cs => !cs.ready)) {
|
|
17
|
+
event = 'DELETED';
|
|
18
|
+
}
|
|
19
|
+
if (event === 'ADDED' || event === 'MODIFIED') {
|
|
20
|
+
logger.debug(`Kubernetes Pod added or modified: ${pod.metadata.name}, namespace: ${pod.metadata.namespace}`);
|
|
21
|
+
let kubernetesPod = _context.default.runningPods.find(existingPod => existingPod.uid === pod.metadata?.uid);
|
|
22
|
+
if (!kubernetesPod) {
|
|
23
|
+
const containers = [];
|
|
24
|
+
if (pod.spec?.containers) {
|
|
25
|
+
pod.spec.containers.forEach(container => {
|
|
26
|
+
const containerPort = container.ports?.[0]?.containerPort;
|
|
27
|
+
const imageVersion = container.image ? container.image.split(':').pop() : undefined;
|
|
28
|
+
if (containerPort && imageVersion) {
|
|
29
|
+
containers.push({
|
|
30
|
+
containerPort: String(containerPort),
|
|
31
|
+
imageVersion
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
kubernetesPod = {
|
|
37
|
+
uid: pod.metadata.uid,
|
|
38
|
+
name: pod.metadata.name ?? pod.metadata.uid,
|
|
39
|
+
namespace: pod.metadata.namespace,
|
|
40
|
+
labels: {},
|
|
41
|
+
containers,
|
|
42
|
+
lastModified: Date.now()
|
|
43
|
+
};
|
|
44
|
+
_context.default.runningPods.push(kubernetesPod);
|
|
45
|
+
}
|
|
46
|
+
kubernetesPod.labels = pod.metadata.labels ?? {};
|
|
47
|
+
kubernetesPod.lastModified = Date.now();
|
|
48
|
+
|
|
49
|
+
// Update all services
|
|
50
|
+
_context.default.services.forEach(service => (0, _updatePluginPackageService.default)(service, undefined, logger));
|
|
51
|
+
} else if (event === 'DELETED') {
|
|
52
|
+
logger.debug(`Kubernetes Pod removed: ${pod.metadata.name}, namespace: ${pod.metadata.namespace}`);
|
|
53
|
+
const kubernetesPod = _context.default.runningPods.find(existingPid => existingPid.uid === pod.metadata?.uid);
|
|
54
|
+
if (kubernetesPod) {
|
|
55
|
+
_context.default.runningPods = _context.default.runningPods.filter(p => p.uid !== kubernetesPod.uid);
|
|
56
|
+
|
|
57
|
+
// Update all services
|
|
58
|
+
_context.default.services.forEach(service => (0, _updatePluginPackageService.default)(service, undefined, logger));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
exports.default = _default;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _context = _interopRequireDefault(require("../context"));
|
|
8
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
var _default = (service, logger) => {
|
|
10
|
+
_context.default.services = _context.default.services.filter(s => s.uid !== service.uid);
|
|
11
|
+
if (service.url) {
|
|
12
|
+
_context.default.scannerCallback?.removePackageUrl(service.url);
|
|
13
|
+
}
|
|
14
|
+
logger.info(`Kubernetes service with remote plugin package removed: ${service.name}`);
|
|
15
|
+
};
|
|
16
|
+
exports.default = _default;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _url = require("url");
|
|
8
|
+
var _context = _interopRequireDefault(require("../context"));
|
|
9
|
+
var _updatePluginPackageService = _interopRequireDefault(require("./updatePluginPackageService"));
|
|
10
|
+
var _removePluginPackageService = _interopRequireDefault(require("./removePluginPackageService"));
|
|
11
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
var _default = (serviceNameFilter, logger) => (event, service) => {
|
|
13
|
+
if (!service.metadata) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (!service.metadata.name?.match(serviceNameFilter)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const {
|
|
20
|
+
name,
|
|
21
|
+
namespace,
|
|
22
|
+
annotations = {}
|
|
23
|
+
} = service.metadata;
|
|
24
|
+
if (event === 'ADDED' || event === 'MODIFIED') {
|
|
25
|
+
logger.debug(`Kubernetes Service added or modified: ${service.metadata.name}, namespace: ${namespace}`);
|
|
26
|
+
let kubernetesService = _context.default.services.find(existingService => existingService.uid === service.metadata?.uid);
|
|
27
|
+
if (!kubernetesService) {
|
|
28
|
+
kubernetesService = {
|
|
29
|
+
uid: service.metadata.uid ?? service.metadata.name,
|
|
30
|
+
name,
|
|
31
|
+
namespace,
|
|
32
|
+
annotations,
|
|
33
|
+
firstSeen: Date.now(),
|
|
34
|
+
targetPort: undefined,
|
|
35
|
+
url: undefined,
|
|
36
|
+
selector: service.spec?.selector,
|
|
37
|
+
runningPods: 0,
|
|
38
|
+
imageVersion: undefined,
|
|
39
|
+
lastModified: Date.now(),
|
|
40
|
+
error: null
|
|
41
|
+
};
|
|
42
|
+
_context.default.services.push(kubernetesService);
|
|
43
|
+
}
|
|
44
|
+
const previousUrl = kubernetesService.url;
|
|
45
|
+
const headlessService = service?.spec?.clusterIP?.toLowerCase() === 'none';
|
|
46
|
+
const ports = service?.spec?.ports;
|
|
47
|
+
const port = ports && ports.length > 0 ? ports[0].port : undefined;
|
|
48
|
+
const targetPort = ports && ports.length > 0 ? String(ports[0].targetPort) : undefined;
|
|
49
|
+
let url;
|
|
50
|
+
if (!headlessService) {
|
|
51
|
+
let serviceUrl = `http://${name}`;
|
|
52
|
+
if (namespace) {
|
|
53
|
+
serviceUrl += `.${namespace}`;
|
|
54
|
+
}
|
|
55
|
+
if (port && port !== 80) {
|
|
56
|
+
serviceUrl += `:${port}`;
|
|
57
|
+
}
|
|
58
|
+
url = new _url.URL(serviceUrl);
|
|
59
|
+
}
|
|
60
|
+
kubernetesService.targetPort = targetPort;
|
|
61
|
+
kubernetesService.url = url;
|
|
62
|
+
kubernetesService.selector = service.spec?.selector;
|
|
63
|
+
kubernetesService.lastModified = Date.now();
|
|
64
|
+
if (headlessService) {
|
|
65
|
+
kubernetesService.error = 'Headless Service';
|
|
66
|
+
}
|
|
67
|
+
(0, _updatePluginPackageService.default)(kubernetesService, previousUrl, logger);
|
|
68
|
+
} else if (event === 'DELETED') {
|
|
69
|
+
logger.debug(`Kubernetes Service removed: ${service.metadata.name}, namespace: ${namespace}`);
|
|
70
|
+
const kubernetesService = _context.default.services.find(existingService => existingService.uid === service.metadata?.uid);
|
|
71
|
+
if (kubernetesService) {
|
|
72
|
+
(0, _removePluginPackageService.default)(kubernetesService, logger);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
exports.default = _default;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _semver = _interopRequireDefault(require("semver"));
|
|
8
|
+
var _context = _interopRequireDefault(require("../context"));
|
|
9
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
var _default = (service, previousUrl, logger) => {
|
|
11
|
+
if (!service.url) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const pods = _context.default.runningPods.filter(pod => {
|
|
15
|
+
if (!service.selector || Object.keys(service.selector).length === 0) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return Object.keys(service.selector).every(key => pod.labels[key] === service.selector[key]);
|
|
19
|
+
});
|
|
20
|
+
const containers = pods.map(pod => pod.containers.find(c => c.containerPort === service.targetPort)).filter(c => !!c);
|
|
21
|
+
if (pods.length === 0) {
|
|
22
|
+
// No running pods found
|
|
23
|
+
if (service.runningPods > 0) {
|
|
24
|
+
_context.default.scannerCallback?.removePackageUrl(service.url);
|
|
25
|
+
logger.info(`Remote plugin package removed because not running Pods found for Kubernetes Service: ${service.name}`);
|
|
26
|
+
}
|
|
27
|
+
service.runningPods = 0;
|
|
28
|
+
service.imageVersion = undefined;
|
|
29
|
+
service.error = 'No running Pods';
|
|
30
|
+
} else {
|
|
31
|
+
const urlChange = previousUrl && previousUrl.toString() !== service.url.toString();
|
|
32
|
+
const highestImageVersion = containers.sort((p1, p2) => _semver.default.gt(p1.imageVersion, p2.imageVersion) ? -1 : 1)[0].imageVersion;
|
|
33
|
+
if (service.imageVersion !== highestImageVersion) {
|
|
34
|
+
logger.info(`Image version changed for Kubernetes Service: ${service.name}. Image version: ${service.imageVersion} -> ${highestImageVersion}`);
|
|
35
|
+
} else if (urlChange) {
|
|
36
|
+
logger.info(`URL changed for Kubernetes Service: ${service.name}. URL: ${previousUrl} -> ${service.url}`);
|
|
37
|
+
}
|
|
38
|
+
if (urlChange) {
|
|
39
|
+
_context.default.scannerCallback?.removePackageUrl(previousUrl);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Add or update the remote package in any case, which is the safest approach
|
|
43
|
+
// (even if the number of Pods didn't change)
|
|
44
|
+
_context.default.scannerCallback?.addOrUpdatePackageUrl(service.url, {
|
|
45
|
+
packageName: service.name,
|
|
46
|
+
packageVersion: highestImageVersion,
|
|
47
|
+
...service.annotations
|
|
48
|
+
});
|
|
49
|
+
service.lastModified = Date.now();
|
|
50
|
+
service.runningPods = pods.length;
|
|
51
|
+
service.imageVersion = highestImageVersion;
|
|
52
|
+
service.error = null;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
exports.default = _default;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _url = require("url");
|
|
4
|
+
var _express = _interopRequireDefault(require("express"));
|
|
5
|
+
var _KubernetesRemotePluginPackagesScanner = require("../scanner/KubernetesRemotePluginPackagesScanner");
|
|
6
|
+
var _context = _interopRequireDefault(require("../context"));
|
|
7
|
+
var _webapp = _interopRequireDefault(require("./webapp"));
|
|
8
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
const testServices = [{
|
|
10
|
+
uid: '1',
|
|
11
|
+
name: 'Service 1',
|
|
12
|
+
namespace: 'namespace1',
|
|
13
|
+
annotations: {},
|
|
14
|
+
targetPort: '1234',
|
|
15
|
+
url: new _url.URL('http://service1.namespace1:8080'),
|
|
16
|
+
selector: {},
|
|
17
|
+
firstSeen: Date.now(),
|
|
18
|
+
lastModified: Date.now(),
|
|
19
|
+
error: null,
|
|
20
|
+
imageVersion: undefined,
|
|
21
|
+
runningPods: 1
|
|
22
|
+
}, {
|
|
23
|
+
uid: '2',
|
|
24
|
+
name: 'Service 2',
|
|
25
|
+
namespace: 'namespace1',
|
|
26
|
+
annotations: {},
|
|
27
|
+
targetPort: '1234',
|
|
28
|
+
url: new _url.URL('http://service2.namespace1:8080'),
|
|
29
|
+
selector: {},
|
|
30
|
+
firstSeen: Date.now(),
|
|
31
|
+
lastModified: Date.now(),
|
|
32
|
+
error: null,
|
|
33
|
+
imageVersion: '1.0.1',
|
|
34
|
+
runningPods: 1
|
|
35
|
+
}, {
|
|
36
|
+
uid: '3',
|
|
37
|
+
name: 'Service 3',
|
|
38
|
+
namespace: 'namespace2',
|
|
39
|
+
annotations: {},
|
|
40
|
+
targetPort: '1234',
|
|
41
|
+
url: undefined,
|
|
42
|
+
selector: undefined,
|
|
43
|
+
firstSeen: Date.now(),
|
|
44
|
+
lastModified: Date.now(),
|
|
45
|
+
error: 'Headless Service',
|
|
46
|
+
imageVersion: undefined,
|
|
47
|
+
runningPods: 0
|
|
48
|
+
}];
|
|
49
|
+
_context.default.services = testServices;
|
|
50
|
+
const testPackages = [{
|
|
51
|
+
url: new _url.URL('http://service1.namespace1:8080'),
|
|
52
|
+
scannerName: _KubernetesRemotePluginPackagesScanner.SCANNER_NAME,
|
|
53
|
+
definitionBuilderName: '',
|
|
54
|
+
processedOnce: false,
|
|
55
|
+
status: 'processing',
|
|
56
|
+
lastUpdate: Date.now(),
|
|
57
|
+
updateErrors: null,
|
|
58
|
+
foundPlugins: null
|
|
59
|
+
}, {
|
|
60
|
+
url: new _url.URL('http://service2.namespace1:8080'),
|
|
61
|
+
scannerName: _KubernetesRemotePluginPackagesScanner.SCANNER_NAME,
|
|
62
|
+
definitionBuilderName: '',
|
|
63
|
+
processedOnce: true,
|
|
64
|
+
status: 'processed',
|
|
65
|
+
lastUpdate: Date.now(),
|
|
66
|
+
updateErrors: null,
|
|
67
|
+
foundPlugins: ['App1', 'App2']
|
|
68
|
+
}];
|
|
69
|
+
const contextHolder = {
|
|
70
|
+
getPluginContext: () => ({
|
|
71
|
+
loggerFactory: () => console,
|
|
72
|
+
serverConfig: {
|
|
73
|
+
externalPluginConfigFileNames: ['mashroom']
|
|
74
|
+
},
|
|
75
|
+
services: {
|
|
76
|
+
core: {
|
|
77
|
+
pluginService: {
|
|
78
|
+
getPotentialPluginPackagesByScanner() {
|
|
79
|
+
return testPackages;
|
|
80
|
+
},
|
|
81
|
+
onUnloadOnce: () => {
|
|
82
|
+
// no implementation required
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
};
|
|
89
|
+
const wrapperApp = (0, _express.default)();
|
|
90
|
+
wrapperApp.use((req, res, next) => {
|
|
91
|
+
req.pluginContext = contextHolder.getPluginContext();
|
|
92
|
+
next();
|
|
93
|
+
});
|
|
94
|
+
wrapperApp.get('/', (req, res) => {
|
|
95
|
+
res.redirect('/remote-plugin-packages-kubernetes');
|
|
96
|
+
});
|
|
97
|
+
wrapperApp.use('/remote-plugin-packages-kubernetes', _webapp.default);
|
|
98
|
+
wrapperApp.listen(8083, () => {
|
|
99
|
+
console.log('Listening on 8083');
|
|
100
|
+
});
|
|
101
|
+
wrapperApp.once('error', error => {
|
|
102
|
+
console.error('Failed to start server!', error);
|
|
103
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _webapp = _interopRequireDefault(require("./webapp"));
|
|
8
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
const bootstrap = async () => {
|
|
10
|
+
return _webapp.default;
|
|
11
|
+
};
|
|
12
|
+
var _default = exports.default = bootstrap;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _context = _interopRequireDefault(require("../../context"));
|
|
8
|
+
var _KubernetesRemotePluginPackagesScanner = require("../../scanner/KubernetesRemotePluginPackagesScanner");
|
|
9
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
+
const formatDate = ts => {
|
|
11
|
+
return new Date(ts).toISOString().replace(/T/, ' ').replace(/\..+/, '');
|
|
12
|
+
};
|
|
13
|
+
var _default = (request, response) => {
|
|
14
|
+
const pluginService = request.pluginContext.services.core.pluginService;
|
|
15
|
+
const pluginPackages = pluginService.getPotentialPluginPackagesByScanner(_KubernetesRemotePluginPackagesScanner.SCANNER_NAME);
|
|
16
|
+
const model = {
|
|
17
|
+
baseUrl: request.baseUrl,
|
|
18
|
+
hasErrors: _context.default.errors.length > 0,
|
|
19
|
+
errors: _context.default.errors,
|
|
20
|
+
namespaceLabelSelector: _context.default.namespaceLabelSelector,
|
|
21
|
+
watchedNamespaces: _context.default.watchedNamespaces.map(({
|
|
22
|
+
name
|
|
23
|
+
}) => name).join(', '),
|
|
24
|
+
serviceLabelSelector: _context.default.serviceLabelSelector,
|
|
25
|
+
serviceNameFilter: _context.default.serviceNameFilter,
|
|
26
|
+
services: _context.default.services.map(service => {
|
|
27
|
+
const pluginPackage = pluginPackages.find(p => service.url && p.url.toString() === service.url.toString());
|
|
28
|
+
const errors = service.error ?? pluginPackage?.updateErrors?.join(', ');
|
|
29
|
+
let status = 'Unknown';
|
|
30
|
+
if (pluginPackage) {
|
|
31
|
+
status = pluginPackage.processedOnce ? pluginPackage.status : 'pending';
|
|
32
|
+
}
|
|
33
|
+
let statusClass = 'pending';
|
|
34
|
+
if (errors) {
|
|
35
|
+
status = 'Error';
|
|
36
|
+
statusClass = 'error';
|
|
37
|
+
} else if (pluginPackage?.status === 'processing') {
|
|
38
|
+
status = 'Processing';
|
|
39
|
+
statusClass = 'processing';
|
|
40
|
+
} else if (pluginPackage?.status === 'processed') {
|
|
41
|
+
status = 'Processed';
|
|
42
|
+
statusClass = 'processed';
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
name: service.name,
|
|
46
|
+
namespace: service.namespace ?? '',
|
|
47
|
+
url: service.url ? service.url.toString() : '',
|
|
48
|
+
runningPods: service.runningPods,
|
|
49
|
+
imageVersion: service.imageVersion,
|
|
50
|
+
status,
|
|
51
|
+
statusClass,
|
|
52
|
+
lastModified: formatDate(service.lastModified),
|
|
53
|
+
rowClass: errors ? 'row-error' : '',
|
|
54
|
+
portalApps: pluginPackage?.foundPlugins ?? undefined,
|
|
55
|
+
errors
|
|
56
|
+
};
|
|
57
|
+
})
|
|
58
|
+
};
|
|
59
|
+
response.render('services', model);
|
|
60
|
+
};
|
|
61
|
+
exports.default = _default;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.default = void 0;
|
|
7
|
+
var _path = _interopRequireDefault(require("path"));
|
|
8
|
+
var _express = _interopRequireDefault(require("express"));
|
|
9
|
+
var _expressHandlebars = require("express-handlebars");
|
|
10
|
+
var _services = _interopRequireDefault(require("./routes/services"));
|
|
11
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
const app = (0, _express.default)();
|
|
13
|
+
app.engine('handlebars', (0, _expressHandlebars.engine)({
|
|
14
|
+
defaultLayout: ''
|
|
15
|
+
}));
|
|
16
|
+
app.set('view engine', 'handlebars');
|
|
17
|
+
app.set('views', _path.default.resolve(__dirname, '../../views'));
|
|
18
|
+
app.use(_express.default.static(_path.default.resolve(__dirname, '../../public')));
|
|
19
|
+
app.get('/', _services.default);
|
|
20
|
+
var _default = exports.default = app;
|
package/mashroom.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../node_modules/@mashroom/mashroom-json-schemas/schemas/mashroom-plugins.json",
|
|
3
|
+
"devModeBuildScript": "build",
|
|
4
|
+
"plugins": [
|
|
5
|
+
{
|
|
6
|
+
"name": "Mashroom Remote Package Scanner Kubernetes",
|
|
7
|
+
"type": "plugin-package-scanner",
|
|
8
|
+
"bootstrap": "./dist/scanner/mashroom-bootstrap-plugin-package-scanner.js",
|
|
9
|
+
"defaultConfig": {
|
|
10
|
+
"priority": 50,
|
|
11
|
+
"namespaceLabelSelector": ["microfrontends=true"],
|
|
12
|
+
"namespaces": null,
|
|
13
|
+
"serviceLabelSelector": ["microfrontends=true"],
|
|
14
|
+
"serviceNameFilter": ".*"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "Mashroom Remote Package Scanner Kubernetes Admin Webapp",
|
|
19
|
+
"type": "web-app",
|
|
20
|
+
"bootstrap": "./dist/webapp/mashroom-bootstrap-webapp.js",
|
|
21
|
+
"defaultConfig": {
|
|
22
|
+
"path": "/remote-plugin-packages-kubernetes"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "Mashroom Remote Package Scanner Kubernetes Admin Webapp Integration",
|
|
27
|
+
"type": "admin-ui-integration",
|
|
28
|
+
"requires": [
|
|
29
|
+
"Mashroom Remote Package Scanner Kubernetes Admin Webapp"
|
|
30
|
+
],
|
|
31
|
+
"target": "Mashroom Remote Package Scanner Kubernetes Admin Webapp",
|
|
32
|
+
"defaultConfig": {
|
|
33
|
+
"order": 100,
|
|
34
|
+
"menuTitle": "Remote Plugin Packages K8S",
|
|
35
|
+
"path": "/remote-plugin-packages-kubernetes"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mashroom/mashroom-remote-package-scanner-kubernetes",
|
|
3
|
+
"description": "A Mashroom Portal plugin that scans for remote plugin packages in a Kubernetes cluster",
|
|
4
|
+
"homepage": "https://www.mashroom-server.com",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/nonblocking/mashroom.git"
|
|
8
|
+
},
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"version": "3.0.0-alpha.0",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/**",
|
|
13
|
+
"public/**",
|
|
14
|
+
"views/**",
|
|
15
|
+
"mashroom.json"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@kubernetes/client-node": "^1.4.0",
|
|
19
|
+
"express-handlebars": "^9.0.1",
|
|
20
|
+
"handlebars": "^4.7.9",
|
|
21
|
+
"semver": "^7.8.5"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@mashroom/mashroom": "3.0.0-alpha.0",
|
|
25
|
+
"@mashroom/mashroom-monitoring-metrics-collector": "3.0.0-alpha.0",
|
|
26
|
+
"@mashroom/mashroom-utils": "3.0.0-alpha.0",
|
|
27
|
+
"@types/semver": "^7.7.1"
|
|
28
|
+
},
|
|
29
|
+
"jest": {
|
|
30
|
+
"testEnvironment": "node",
|
|
31
|
+
"roots": [
|
|
32
|
+
"<rootDir>/test"
|
|
33
|
+
],
|
|
34
|
+
"testRegex": "(\\.(test|spec))\\.ts",
|
|
35
|
+
"transform": {
|
|
36
|
+
"^.+\\.ts$": "ts-jest"
|
|
37
|
+
},
|
|
38
|
+
"reporters": [
|
|
39
|
+
"default",
|
|
40
|
+
"jest-junit"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"jest-junit": {
|
|
44
|
+
"outputDirectory": "./test-reports"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"lint": "eslint --fix",
|
|
48
|
+
"type-check": "tsc --noEmit",
|
|
49
|
+
"test": "jest",
|
|
50
|
+
"build": "babel src/js --extensions \".ts\" -d dist && sass --style compressed src/sass/style.scss public/style.css",
|
|
51
|
+
"dev": "nodemon --watch src -e ts,scss --exec \"npm run build && node dist/webapp/dev-server.js\""
|
|
52
|
+
}
|
|
53
|
+
}
|
package/public/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
body{text-rendering:optimizeLegibility;font-size:16px;line-height:1.5;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;min-height:800px;margin:16px}h2{font-size:1.2em;font-weight:normal}a,a:visited,a:focus{color:#4d487f;text-decoration:none;transition:color 500ms ease-out,background-color 500ms ease-out}a:hover,a:visited:hover,a:focus:hover{color:#3a365f}a:active,a:visited:active,a:focus:active{color:#3a365f}.errors{margin:20px 0;color:red}.errors ul{margin:5px 0}.errors li{padding:2px 0}.footnotes{margin-top:20px;font-size:.9em}table,th,td{border:none}table{border-spacing:0;border-collapse:collapse}tr:nth-child(even){background-color:#e2e2e2}th{padding:8px 10px;text-align:left;vertical-align:top;white-space:nowrap;font-weight:700}td{padding:8px 10px;vertical-align:top}tr.row-error{background-color:#fdd}tr.row-ignored{color:#666}.error{color:red}.pending,.processing{color:orange}/*# sourceMappingURL=style.css.map */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sourceRoot":"","sources":["../src/sass/style.scss"],"names":[],"mappings":"AAYA,KACI,kCACA,eACA,gBACA,YAVW,4CAWX,iBACA,YAGJ,GACI,gBACA,mBAGJ,oBACI,MA1BY,QA2BZ,qBAnBA,gEAsBA,sCACI,MA7Ba,QAgCjB,yCACI,MAjCa,QAqCrB,QACI,cACA,MAtCU,IAwCV,WACI,aAGJ,WACI,cAIR,WACI,gBACA,eAGJ,YACI,YAGJ,MACI,iBACA,yBAGJ,mBACI,yBAGJ,GACI,iBACA,gBACA,mBACA,mBACA,gBAGJ,GACI,iBACA,mBAIJ,aACE,sBAGF,eACE,WAGF,OACE,MA1FY,IA6Fd,qBACE","file":"style.css"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
|
5
|
+
<meta charset="utf-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
7
|
+
|
|
8
|
+
<link rel="stylesheet" type="text/css" href="{{baseUrl}}/style.css">
|
|
9
|
+
|
|
10
|
+
<script type="application/javascript">
|
|
11
|
+
function openPlugin(pluginName) {
|
|
12
|
+
window.top.location.href = `/mashroom/admin/plugins?q=${encodeURIComponent(pluginName)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
var lastContentHeight = null;
|
|
16
|
+
function sendHeight() {
|
|
17
|
+
var contentHeight = document.body.offsetHeight;
|
|
18
|
+
if (lastContentHeight !== contentHeight) {
|
|
19
|
+
parent.postMessage({
|
|
20
|
+
height: contentHeight + 20
|
|
21
|
+
}, "*");
|
|
22
|
+
lastContentHeight = contentHeight;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (parent !== window) {
|
|
27
|
+
setInterval(sendHeight, 1000);
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<title>Mashroom Kubernetes Remote Packages</title>
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
<h2>Kubernetes Remote Packages</h2>
|
|
35
|
+
|
|
36
|
+
{{#if hasErrors}}
|
|
37
|
+
<div class="errors">
|
|
38
|
+
<div class="errors-header">Scan Errors:</div>
|
|
39
|
+
<ul>
|
|
40
|
+
{{#each errors}}
|
|
41
|
+
<li>{{this}}</li>
|
|
42
|
+
{{/each}}
|
|
43
|
+
</ul>
|
|
44
|
+
</div>
|
|
45
|
+
{{/if}}
|
|
46
|
+
|
|
47
|
+
<div class="services">
|
|
48
|
+
<table>
|
|
49
|
+
<tr>
|
|
50
|
+
<th>Name</th>
|
|
51
|
+
<th>Namespace</th>
|
|
52
|
+
<th>URL</th>
|
|
53
|
+
<th>Valid Pods</th>
|
|
54
|
+
<th>Image Version</th>
|
|
55
|
+
<th>Last Modified</th>
|
|
56
|
+
<th>Status</th>
|
|
57
|
+
<th>Portal Apps</th>
|
|
58
|
+
</tr>
|
|
59
|
+
{{#each services}}
|
|
60
|
+
<tr class="{{this.rowClass}}">
|
|
61
|
+
<td>{{this.name}}</td>
|
|
62
|
+
<td>{{this.namespace}}</td>
|
|
63
|
+
<td>{{this.url}}</td>
|
|
64
|
+
<td>{{this.runningPods}}</td>
|
|
65
|
+
<td>{{this.imageVersion}}</td>
|
|
66
|
+
<td>{{this.lastModified}}</td>
|
|
67
|
+
<td class="{{this.statusClass}}">{{this.status}}</td>
|
|
68
|
+
<td>
|
|
69
|
+
{{#if this.portalApps}}
|
|
70
|
+
<div>
|
|
71
|
+
{{#each this.portalApps}}
|
|
72
|
+
{{#if @index}}, {{/if}}<a href="javascript:void(0)" onclick="openPlugin('{{this}}')">{{this}}</a>
|
|
73
|
+
{{/each}}
|
|
74
|
+
</div>
|
|
75
|
+
{{/if}}
|
|
76
|
+
{{#if this.errors}}
|
|
77
|
+
<div class="error">{{this.errors}}</div>
|
|
78
|
+
{{/if}}
|
|
79
|
+
</td>
|
|
80
|
+
</tr>
|
|
81
|
+
{{/each}}
|
|
82
|
+
</table>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="footnotes">
|
|
86
|
+
{{#if namespaceLabelSelector}}
|
|
87
|
+
<strong>Namespace label selector:</strong> {{namespaceLabelSelector}}
|
|
88
|
+
<br/>
|
|
89
|
+
{{/if}}
|
|
90
|
+
<strong>Watched Namespaces:</strong> {{watchedNamespaces}}
|
|
91
|
+
<br/>
|
|
92
|
+
{{#if serviceLabelSelector}}
|
|
93
|
+
<strong>Service label selector:</strong> {{serviceLabelSelector}}
|
|
94
|
+
<br/>
|
|
95
|
+
{{/if}}
|
|
96
|
+
<strong>Service name filter:</strong> {{serviceNameFilter}}
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
</body>
|
|
100
|
+
</html>
|