@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 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
+ ```
@@ -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,5 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
@@ -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
+ }
@@ -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>