Skip to content

Commit 0caace2

Browse files
authored
feat(action): add deployment action for registering with Cryostat (cryostatio#411)
1 parent b94408e commit 0caace2

19 files changed

Lines changed: 3689 additions & 165 deletions

.github/workflows/ci.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,24 @@ jobs:
6161
- uses: bahmutov/npm-install@v1
6262
- run: yarn type-check:plugin
6363

64+
test:
65+
runs-on: ubuntu-latest
66+
strategy:
67+
matrix:
68+
node-version: ['lts/*']
69+
steps:
70+
- uses: actions/checkout@v4
71+
with:
72+
submodules: true
73+
fetch-depth: 0
74+
- name: Use Node.js ${{ matrix.node-version }}
75+
uses: actions/setup-node@v3
76+
with:
77+
node-version: ${{ matrix.node-version }}
78+
cache: 'yarn'
79+
- uses: bahmutov/npm-install@v1
80+
- run: yarn test:ci
81+
6482
build-container:
6583
strategy:
6684
matrix:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
**/dist
99
**/.DS_Store
1010
.devcontainer/dev.env
11+
coverage
1112

1213
locales/en/plugin__cryostat-plugin.json
1314

console-extensions.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,5 +234,29 @@
234234
"perspective": "admin",
235235
"section": "cryostat-section"
236236
}
237+
},
238+
{
239+
"type": "console.action/resource-provider",
240+
"properties": {
241+
"model": {
242+
"group": "apps",
243+
"version": "v1",
244+
"kind": "Deployment"
245+
},
246+
"provider": {
247+
"$codeRef": "DeploymentLabelActionProvider"
248+
}
249+
}
250+
},
251+
{
252+
"type": "console.topology/decorator/provider",
253+
"properties": {
254+
"id": "cryostat-deployment-decorator",
255+
"priority": 100,
256+
"quadrant": "upperLeft",
257+
"decorator": {
258+
"$codeRef": "getDeploymentDecorator"
259+
}
260+
}
237261
}
238262
]

jest.config.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// For a detailed explanation regarding each configuration property, visit:
2+
// https://jestjs.io/docs/en/configuration.html
3+
4+
module.exports = {
5+
// Automatically clear mock calls and instances between every test
6+
clearMocks: true,
7+
8+
// The directory where Jest should output its coverage files
9+
coverageDirectory: 'coverage',
10+
11+
// An array of directory names to be searched recursively up from the requiring module's location
12+
moduleDirectories: [
13+
"node_modules",
14+
"<rootDir>/src"
15+
],
16+
17+
// An array of file extensions your modules use
18+
moduleFileExtensions: [
19+
"ts",
20+
"tsx",
21+
"js"
22+
],
23+
24+
// A map from regular expressions to module names that allow to stub out resources with a single module
25+
moduleNameMapper: {
26+
'\\.(css|less)$': '<rootDir>/src/cryostat-web/__mocks__/styleMock.js',
27+
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/src/cryostat-web/__mocks__/fileMock.js",
28+
"@app/(.*)": '<rootDir>/src/cryostat-web/src/app/$1',
29+
"@i18n/(.*)": '<rootDir>/src/cryostat-web/src/i18n/$1',
30+
'@console-plugin/(.*)$': '<rootDir>/src/openshift/$1',
31+
},
32+
33+
// A preset that is used as a base for Jest's configuration
34+
preset: "ts-jest/presets/js-with-ts",
35+
36+
// The path to a module that runs some code to configure or set up the testing framework before each test
37+
setupFilesAfterEnv: ["@testing-library/jest-dom"],
38+
39+
// The test environment that will be used for testing.
40+
testEnvironment: "jsdom",
41+
42+
// The glob patterns Jest uses to detect test files
43+
testMatch: [
44+
"<rootDir>/src/openshift/**/*.test.(ts|tsx)"
45+
],
46+
47+
// ts-jest config option isolatedModules is deprecated and will be removed in v30
48+
// "isolatedModules" is now set in tsconfig-jest.json
49+
transform: {
50+
'^.+.tsx?$': ['ts-jest', {
51+
tsconfig: 'tsconfig-jest.json'
52+
}]
53+
},
54+
55+
// An array of regexp pattern strings that are matched against all source file paths before transformation.
56+
// If the file path matches any of the patterns, it will not be transformed.
57+
transformIgnorePatterns: [
58+
"/node_modules/(?!(@openshift-console\\S*?|@openshift/dynamic-plugin-sdk-utils|@patternfly|d3|d3-array|internmap|delaunator|robust-predicates|uuid))",
59+
],
60+
61+
roots: ['<rootDir>/src']
62+
};

locales/en/common.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,13 @@
1010
"Navigation.Events": "Events",
1111
"Navigation.Instrumentation": "Instrumentation",
1212
"Navigation.Security": "Security",
13-
"Navigation.About": "About"
13+
"Navigation.About": "About",
14+
"DEPLOYMENT_ACTION_TITLE": "Register with Cryostat",
15+
"DEPLOYMENT_ACTION_ALREADY_REGISTERED": "Deployment is already registered with this option",
16+
"DEPLOYMENT_ACTION_SELECT_LABEL": "Select a Cryostat instance:",
17+
"DEPLOYMENT_ACTION_EMPTY_OPTION": "<None>",
18+
"DEPLOYMENT_ACTION_NAMESPACE_NOT_A_TARGET_NAMESPACE": "Warning: {{deploymentNamespace}} is not in the list of target namespaces for {{cryostatName}}",
19+
"DEPLOYMENT_ACTION_NO_UPDATE_PERMISSIONS": "Warning: user does not have permissions to update deployment: {{deploymentName}}",
20+
"DEPLOYMENT_ACTION_NO_CRYOSTAT_OPERATOR_CR": "Warning: this registration feature requires at least one Cryostat custom resource associated with a Cryostat Operator",
21+
"DEPLOYMENT_ACTION_HELM_CRYOSTAT_SELECTED": "Warning: {{cryostatName}} was installed using a Helm chart. This feature requires the Cryostat Operator for dynamic attachment."
1422
}

package.json

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"start": "yarn webpack serve --progress",
3434
"start-console": "./start-console.sh",
3535
"i18n": "./i18n-scripts/build-i18n.sh && node ./i18n-scripts/set-english-defaults.js",
36+
"test": "jest src/openshift --maxWorkers=50% --coverage",
37+
"test:ci": "jest src/openshift --maxWorkers=50%",
3638
"cypress": "cd cypress && cypress open",
3739
"cypress:headed": "cd cypress && cypress run --headed",
3840
"cypress:headless": "cd cypress && cypress run --headless",
@@ -46,6 +48,9 @@
4648
"@cypress/webpack-preprocessor": "^6.0.4",
4749
"@openshift-console/dynamic-plugin-sdk": "1.4.0",
4850
"@openshift-console/dynamic-plugin-sdk-webpack": "1.1.1",
51+
"@openshift/dynamic-plugin-sdk": "5.0.1",
52+
"@openshift/dynamic-plugin-sdk-extensions": "1.4.1",
53+
"@openshift/dynamic-plugin-sdk-utils": "5.0.1",
4954
"@patternfly/patternfly": "^5.4.0",
5055
"@patternfly/quickstarts": "^5.4.0",
5156
"@patternfly/react-catalog-view-extension": "^5.0.0",
@@ -57,6 +62,10 @@
5762
"@patternfly/react-topology": "^5.4.0",
5863
"@reduxjs/toolkit": "^1.9.3",
5964
"@stoplight/prism-cli": "^5.14.2",
65+
"@testing-library/jest-dom": "^6.7.0",
66+
"@testing-library/react": "^12.1.2",
67+
"@testing-library/user-event": "^14.6.1",
68+
"@types/jest": "^30.0.0",
6069
"@types/node": "^22.10.5",
6170
"@types/react": "^17.0.69",
6271
"@types/react-helmet": "^6.1.4",
@@ -80,6 +89,8 @@
8089
"i18next": "^21.8.4",
8190
"i18next-browser-languagedetector": "^8.2.0",
8291
"i18next-parser": "^9.3.0",
92+
"jest": "^29.7.0",
93+
"jest-environment-jsdom": "^30.0.5",
8394
"mocha-junit-reporter": "^2.2.0",
8495
"mochawesome": "^7.1.4",
8596
"mochawesome-merge": "^5.0.0",
@@ -96,12 +107,13 @@
96107
"style-loader": "^4.0.0",
97108
"stylelint": "^16.25.0",
98109
"stylelint-config-standard": "^39.0.1",
110+
"ts-jest": "^29.4.1",
99111
"ts-loader": "^9.5.4",
100112
"ts-node": "^10.8.1",
101113
"tsconfig-paths-webpack-plugin": "^4.1.0",
102114
"typescript": "^5.8.3",
103115
"webpack": "5.75.0",
104-
"webpack-cli": "^4.9.2",
116+
"webpack-cli": "^5.1.4",
105117
"webpack-dev-server": "^5.2.2"
106118
},
107119
"dependencies": {
@@ -128,7 +140,9 @@
128140
"ArchivesPage": "./openshift/pages/ArchivesPage",
129141
"EventsPage": "./openshift/pages/EventsPage",
130142
"InstrumentationPage": "./openshift/pages/InstrumentationPage",
131-
"SecurityPage": "./openshift/pages/SecurityPage"
143+
"SecurityPage": "./openshift/pages/SecurityPage",
144+
"DeploymentLabelActionProvider": "./openshift/actions/DeploymentLabelAction/DeploymentLabelActionProvider",
145+
"getDeploymentDecorator": "./openshift/actions/DeploymentLabelAction/getDeploymentDecorator"
132146
},
133147
"dependencies": {
134148
"@console/pluginAPI": "*"
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/*
2+
* Copyright The Cryostat Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import CryostatIcon from '@console-plugin/assets/CryostatIcon';
17+
import { k8sGet, K8sResourceKind, useK8sModel, useK8sWatchResource } from '@openshift-console/dynamic-plugin-sdk';
18+
import { Node } from '@patternfly/react-topology';
19+
import * as React from 'react';
20+
21+
type DeploymentDecoratorProps = {
22+
element: Node;
23+
radius: number;
24+
x: number;
25+
y: number;
26+
};
27+
28+
export const DeploymentDecorator: React.FC<DeploymentDecoratorProps> = ({ element, radius, x, y }) => {
29+
const [routeModel] = useK8sModel({ group: 'route.openshift.io', version: 'v1', kind: 'Route' });
30+
const routeUrl = React.useRef('');
31+
const [isRegistered, setIsRegistered] = React.useState(false);
32+
const [deployment, deploymentLoaded] = useK8sWatchResource<K8sResourceKind>({
33+
groupVersionKind: {
34+
group: 'apps',
35+
version: 'v1',
36+
kind: 'Deployment',
37+
},
38+
name: element['resource']?.metadata?.name || undefined,
39+
namespace: element['resource']?.metadata?.namespace || undefined,
40+
});
41+
const [cryostats, cryostatsLoaded] = useK8sWatchResource<K8sResourceKind[]>({
42+
groupVersionKind: {
43+
group: 'operator.cryostat.io',
44+
version: 'v1beta2',
45+
kind: 'Cryostat',
46+
},
47+
isList: true,
48+
});
49+
50+
React.useEffect(() => {
51+
if (deploymentLoaded && cryostatsLoaded) {
52+
const deploymentLabels = deployment.spec?.template.metadata.labels;
53+
if (deploymentLabels && deploymentLabels['cryostat.io/name'] && deploymentLabels['cryostat.io/namespace']) {
54+
setIsRegistered(true);
55+
} else {
56+
setIsRegistered(false);
57+
}
58+
}
59+
}, [cryostats, cryostatsLoaded, deployment, deploymentLoaded]);
60+
61+
React.useEffect(() => {
62+
if (deploymentLoaded && cryostatsLoaded && isRegistered) {
63+
const labels = deployment.spec?.template.metadata.labels;
64+
if (labels && labels['cryostat.io/name'] && labels['cryostat.io/namespace']) {
65+
cryostats.forEach((cryostat) => {
66+
if (
67+
cryostat.metadata?.name === labels['cryostat.io/name'] &&
68+
cryostat.metadata?.namespace === labels['cryostat.io/namespace']
69+
) {
70+
k8sGet({
71+
model: routeModel,
72+
name: labels['cryostat.io/name'],
73+
ns: labels['cryostat.io/namespace'],
74+
})
75+
.catch(() => '')
76+
.then(
77+
(route: any) => {
78+
const ingresses = route?.status?.ingress;
79+
let res = '';
80+
if (ingresses && ingresses?.length > 0 && ingresses[0]?.host) {
81+
res = `http://${ingresses[0].host}`;
82+
}
83+
routeUrl.current = res;
84+
},
85+
() => {
86+
routeUrl.current = '';
87+
},
88+
);
89+
}
90+
});
91+
}
92+
}
93+
}, [cryostats, cryostatsLoaded, deployment, deploymentLoaded, isRegistered, routeModel]);
94+
95+
if (element['resourceKind'] === 'apps~v1~Deployment' && isRegistered) {
96+
return (
97+
<a
98+
className="odc-decorator__link"
99+
href={`${routeUrl.current}/topology`}
100+
target="_blank"
101+
rel="noopener noreferrer"
102+
role="button"
103+
aria-label="Open Cryostat"
104+
>
105+
<g className="pf-topology__node__decorator odc-decorator cryostat-decorator">
106+
<circle className="pf-topology__node__decorator__bg" cx={x} cy={y} r={radius}></circle>
107+
<g transform={`translate(${x}, ${y})`}>
108+
<g transform="translate(-6.5, -6.5)">
109+
<CryostatIcon width={`${radius}px`} height={`${radius}px`}></CryostatIcon>
110+
</g>
111+
</g>
112+
</g>
113+
</a>
114+
);
115+
}
116+
return <></>;
117+
};
118+
119+
export default DeploymentDecorator;

0 commit comments

Comments
 (0)