├── .ci-operator.yaml ├── .devcontainer ├── Dockerfile.console ├── Dockerfile.plugin ├── devcontainer.json ├── docker-compose.yml └── init.sh ├── .eslintrc.yml ├── .gitignore ├── .prettierrc.yml ├── .stylelintrc.yaml ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── OWNERS ├── README.md ├── charts └── openshift-console-plugin │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ ├── _helpers.tpl │ ├── configmap.yaml │ ├── consoleplugin.yaml │ ├── deployment.yaml │ ├── patch-consoles-job.yaml │ ├── patcher-clusterrole.yaml │ ├── patcher-clusterrolebinding.yaml │ ├── patcher-serviceaccount.yaml │ ├── service.yaml │ └── serviceaccount.yaml │ └── values.yaml ├── console-extensions.json ├── ct.yaml ├── i18n-scripts ├── build-i18n.sh ├── common.js ├── lexers.js └── set-english-defaults.js ├── i18next-parser.config.js ├── install_helm.sh ├── integration-tests ├── .eslintrc ├── cypress.config.js ├── fixtures │ └── example.json ├── plugins │ └── index.ts ├── reporter-config.json ├── support │ ├── index.ts │ └── login.ts ├── tests │ └── example-page.cy.ts └── tsconfig.json ├── locales └── en │ └── plugin__console-plugin-template.json ├── package.json ├── src └── components │ ├── ExamplePage.tsx │ └── example.css ├── start-console.sh ├── test-frontend.sh ├── test-prow-e2e.sh ├── tsconfig.json ├── webpack.config.ts └── yarn.lock /.ci-operator.yaml: -------------------------------------------------------------------------------- 1 | build_root_image: 2 | name: nodejs-18 3 | namespace: openshift 4 | tag: latest -------------------------------------------------------------------------------- /.devcontainer/Dockerfile.console: -------------------------------------------------------------------------------- 1 | FROM quay.io/openshift/origin-console:latest 2 | COPY --from=openshift/origin-cli:latest /usr/bin/oc /usr/local/bin/oc 3 | 4 | ENV OC_URL=$OC_URL 5 | ENV OC_PASS=$OC_PASS 6 | ENV OC_USER=$OC_USER 7 | ENV OC_PLUGIN_NAME=$OC_PLUGIN_NAME 8 | 9 | USER root 10 | CMD eval "oc login $OC_URL -u $OC_USER -p $OC_PASS --insecure-skip-tls-verify" && \ 11 | /opt/bridge/bin/bridge -public-dir=/opt/bridge/static \ 12 | -i18n-namespaces plugin__$OC_PLUGIN_NAME \ 13 | -plugins $OC_PLUGIN_NAME=http://localhost:9001 \ 14 | -k8s-mode-off-cluster-thanos=$(oc -n openshift-config-managed get configmap monitoring-shared-config -o jsonpath='{.data.thanosPublicURL}') \ 15 | -k8s-mode-off-cluster-endpoint=$(oc whoami --show-server) \ 16 | -k8s-mode-off-cluster-skip-verify-tls=true \ 17 | -k8s-auth-bearer-token=$(oc whoami --show-token) \ 18 | -k8s-auth="bearer-token" \ 19 | -user-auth="disabled" \ 20 | -k8s-mode="off-cluster" 21 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile.plugin: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:16 as build 2 | COPY --from=openshift/origin-cli:latest /usr/bin/oc /usr/local/bin/oc 3 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Console + Plugin", 3 | "dockerComposeFile": "docker-compose.yml", 4 | "service": "plugin", 5 | "workspaceFolder": "/workspace", 6 | 7 | "initializeCommand": ".devcontainer/init.sh", 8 | "postCreateCommand": "yarn && eval 'oc login $OC_URL -u $OC_USER -p $OC_PASS --insecure-skip-tls-verify'", 9 | "forwardPorts": [9000, 9001], 10 | "portsAttributes": { 11 | "9000": { 12 | "label": "Console" 13 | }, 14 | "9001": { 15 | "label": "Plugin static files", 16 | "onAutoForward": "silent" 17 | } 18 | }, 19 | "features": {}, 20 | "customizations": { 21 | "vscode": { 22 | "settings": {}, 23 | "extensions": [ 24 | "ms-azuretools.vscode-docker", 25 | "ms-vscode.vscode-typescript-next", 26 | "dbaeumer.vscode-eslint", 27 | "esbenp.prettier-vscode" 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | # create dev.env with the following values: 3 | # OC_URL 4 | # OC_USER 5 | # OC_PASS 6 | # OC_PLUGIN_NAME 7 | services: 8 | console: 9 | build: 10 | context: .. 11 | dockerfile: .devcontainer/Dockerfile.console 12 | env_file: dev.env 13 | restart: unless-stopped 14 | healthcheck: 15 | test: oc whoami 16 | interval: 1m30s 17 | timeout: 10s 18 | retries: 5 19 | 20 | plugin: 21 | build: 22 | context: .. 23 | dockerfile: .devcontainer/Dockerfile.plugin 24 | env_file: dev.env 25 | depends_on: 26 | - console 27 | network_mode: service:console 28 | # Overrides default command so things don't shut down after the process ends. 29 | command: sleep infinity 30 | # Cache local workspace and copy shell history. 31 | volumes: 32 | - ..:/workspace:cached 33 | - ~/.bash_history:/root/.bash_history 34 | - ~/.zsh_history:/root/.zsh_history 35 | -------------------------------------------------------------------------------- /.devcontainer/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ ! -f .devcontainer/dev.env ]] 4 | then 5 | cat << EOF 6 | env file 'dev.env' does not exist in .devcontainer, please create it and add the the correct values for your cluster. 7 | OC_PLUGIN_NAME=my-plugin 8 | OC_URL=https://api.example.com:6443 9 | OC_USER=kubeadmin 10 | OC_PASS= 11 | EOF 12 | exit 2 13 | else 14 | echo 'found 'dev.env' in .devcontainer' 15 | fi 16 | 17 | # if one of the variables are missing, abort the build. 18 | success=1 19 | ! grep -q OC_PLUGIN_NAME= ".devcontainer/dev.env" && success=0 20 | ! grep -q OC_URL= ".devcontainer/dev.env" && success=0 21 | ! grep -q OC_USER= ".devcontainer/dev.env" && success=0 22 | ! grep -q OC_PASS= ".devcontainer/dev.env" && success=0 23 | 24 | if ((success)); then 25 | echo 'dev.env is formatted correctly, proceeding.' 26 | else 27 | cat << EOF 28 | dev.env is not formatted correctly, please add the the correct values for your cluster. 29 | OC_PLUGIN_NAME=my-plugin 30 | OC_URL=https://api.example.com:6443 31 | OC_USER=kubeadmin 32 | OC_PASS= 33 | EOF 34 | exit 2 35 | fi 36 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | browser: true 3 | es2021: true 4 | extends: 5 | - eslint:recommended 6 | - plugin:react/recommended 7 | - plugin:@typescript-eslint/recommended 8 | - prettier 9 | parser: '@typescript-eslint/parser' 10 | parserOptions: 11 | ecmaFeatures: 12 | jsx: true 13 | ecmaVersion: 2016 14 | sourceType: module 15 | plugins: 16 | - prettier 17 | - react 18 | - '@typescript-eslint' 19 | rules: 20 | prettier/prettier: 21 | - error 22 | settings: 23 | react: 24 | version: detect 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/dist 3 | **/.DS_Store 4 | .devcontainer/dev.env 5 | integration-tests/videos 6 | integration-tests/screenshots 7 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | arrowParens: always 2 | printWidth: 100 3 | singleQuote: true 4 | trailingComma: all 5 | -------------------------------------------------------------------------------- /.stylelintrc.yaml: -------------------------------------------------------------------------------- 1 | extends: 2 | - stylelint-config-standard 3 | rules: 4 | # Disallow color names and hex colors as these don't work well with dark mode. 5 | # Use PF global variables instead: 6 | # https://patternfly-react-main.surge.sh/developer-resources/global-css-variables#global-css-variables 7 | color-named: never 8 | color-no-hex: true 9 | # PatternFly CSS vars don't conform to stylelint's regex. Disable this rule. 10 | custom-property-pattern: null 11 | function-disallowed-list: 12 | - rgb 13 | # Disable the standard rule to allow BEM-style classnames with underscores. 14 | selector-class-pattern: null 15 | # Disallow CSS classnames prefixed with .pf- or .co- as these prefixes are 16 | # reserved by PatternFly and OpenShift console. 17 | selector-disallowed-list: 18 | - "*" 19 | - /\.(pf|co)-/ 20 | # Plugins should avoid naked element selectors like `table` and `li` since 21 | # this can impact layout of existing pages in console. 22 | selector-max-type: 23 | - 0 24 | - ignore: 25 | - compounded 26 | - descendant 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "search.exclude": { 4 | "**/node_modules": true 5 | }, 6 | "files.watcherExclude": { 7 | "**/node_modules/**": true 8 | }, 9 | "files.associations": { 10 | "**/console-extensions.json": "jsonc" 11 | }, 12 | "json.schemas": [ 13 | { 14 | "fileMatch": ["**/console-extensions.json"], 15 | "url": "./node_modules/@openshift-console/dynamic-plugin-sdk-webpack/schema/console-extensions.json" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.access.redhat.com/ubi9/nodejs-18:latest AS build 2 | USER root 3 | RUN command -v yarn || npm i -g yarn 4 | 5 | ADD . /usr/src/app 6 | WORKDIR /usr/src/app 7 | RUN yarn install && yarn build 8 | 9 | FROM registry.access.redhat.com/ubi9/nginx-120:latest 10 | 11 | COPY --from=build /usr/src/app/dist /usr/share/nginx/html 12 | USER 1001 13 | 14 | ENTRYPOINT ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Red Hat, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - christianvogt 3 | - florkbr 4 | - spadgett 5 | - vojtechszocs 6 | - jhadvig 7 | - TheRealJon 8 | - rhamilto 9 | component: Management Console 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenShift Console Plugin Template 2 | 3 | This project is a minimal template for writing a new OpenShift Console dynamic 4 | plugin. 5 | 6 | [Dynamic plugins](https://github.com/openshift/console/tree/master/frontend/packages/console-dynamic-plugin-sdk) 7 | allow you to extend the 8 | [OpenShift UI](https://github.com/openshift/console) 9 | at runtime, adding custom pages and other extensions. They are based on 10 | [webpack module federation](https://webpack.js.org/concepts/module-federation/). 11 | Plugins are registered with console using the `ConsolePlugin` custom resource 12 | and enabled in the console operator config by a cluster administrator. 13 | 14 | Using the latest `v1` API version of `ConsolePlugin` CRD, requires OpenShift 4.12 15 | and higher. For using old `v1alpha1` API version us OpenShift version 4.10 or 4.11. 16 | 17 | For an example of a plugin that works with OpenShift 4.11, see the `release-4.11` branch. 18 | For a plugin that works with OpenShift 4.10, see the `release-4.10` branch. 19 | 20 | [Node.js](https://nodejs.org/en/) and [yarn](https://yarnpkg.com) are required 21 | to build and run the example. To run OpenShift console in a container, either 22 | [Docker](https://www.docker.com) or [podman 3.2.0+](https://podman.io) and 23 | [oc](https://console.redhat.com/openshift/downloads) are required. 24 | 25 | ## Getting started 26 | 27 | > [!IMPORTANT] 28 | > To use this template, **DO NOT FORK THIS REPOSITORY**! Click **Use this template**, then select 29 | > [**Create a new repository**](https://github.com/new?template_name=networking-console-plugin&template_owner=openshift) 30 | > to create a new repository. 31 | > 32 | > ![A screenshot showing where the "Use this template" button is located](https://i.imgur.com/AhaySbU.png) 33 | > 34 | > **Forking this repository** for purposes outside of contributing to this repository 35 | > **will cause issues**, as users cannot have more than one fork of a template repository 36 | > at a time. This could prevent future users from forking and contributing to your plugin. 37 | > 38 | > Your fork would also behave like a template repository, which might be confusing for 39 | > contributiors, because it is not possible for repositories generated from a template 40 | > repository to contribute back to the template. 41 | 42 | After cloning your instantiated repository, you must update the plugin metadata, such as the 43 | plugin name in the `consolePlugin` declaration of [package.json](package.json). 44 | 45 | ```json 46 | "consolePlugin": { 47 | "name": "console-plugin-template", 48 | "version": "0.0.1", 49 | "displayName": "My Plugin", 50 | "description": "Enjoy this shiny, new console plugin!", 51 | "exposedModules": { 52 | "ExamplePage": "./components/ExamplePage" 53 | }, 54 | "dependencies": { 55 | "@console/pluginAPI": "*" 56 | } 57 | } 58 | ``` 59 | 60 | The template adds a single example page in the Home navigation section. The 61 | extension is declared in the [console-extensions.json](console-extensions.json) 62 | file and the React component is declared in 63 | [src/components/ExamplePage.tsx](src/components/ExamplePage.tsx). 64 | 65 | You can run the plugin using a local development environment or build an image 66 | to deploy it to a cluster. 67 | 68 | ## Development 69 | 70 | ### Option 1: Local 71 | 72 | In one terminal window, run: 73 | 74 | 1. `yarn install` 75 | 2. `yarn run start` 76 | 77 | In another terminal window, run: 78 | 79 | 1. `oc login` (requires [oc](https://console.redhat.com/openshift/downloads) and an [OpenShift cluster](https://console.redhat.com/openshift/create)) 80 | 2. `yarn run start-console` (requires [Docker](https://www.docker.com) or [podman 3.2.0+](https://podman.io)) 81 | 82 | This will run the OpenShift console in a container connected to the cluster 83 | you've logged into. The plugin HTTP server runs on port 9001 with CORS enabled. 84 | Navigate to to see the running plugin. 85 | 86 | #### Running start-console with Apple silicon and podman 87 | 88 | If you are using podman on a Mac with Apple silicon, `yarn run start-console` 89 | might fail since it runs an amd64 image. You can workaround the problem with 90 | [qemu-user-static](https://github.com/multiarch/qemu-user-static) by running 91 | these commands: 92 | 93 | ```bash 94 | podman machine ssh 95 | sudo -i 96 | rpm-ostree install qemu-user-static 97 | systemctl reboot 98 | ``` 99 | 100 | ### Option 2: Docker + VSCode Remote Container 101 | 102 | Make sure the 103 | [Remote Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) 104 | extension is installed. This method uses Docker Compose where one container is 105 | the OpenShift console and the second container is the plugin. It requires that 106 | you have access to an existing OpenShift cluster. After the initial build, the 107 | cached containers will help you start developing in seconds. 108 | 109 | 1. Create a `dev.env` file inside the `.devcontainer` folder with the correct values for your cluster: 110 | 111 | ```bash 112 | OC_PLUGIN_NAME=console-plugin-template 113 | OC_URL=https://api.example.com:6443 114 | OC_USER=kubeadmin 115 | OC_PASS= 116 | ``` 117 | 118 | 2. `(Ctrl+Shift+P) => Remote Containers: Open Folder in Container...` 119 | 3. `yarn run start` 120 | 4. Navigate to 121 | 122 | ## Docker image 123 | 124 | Before you can deploy your plugin on a cluster, you must build an image and 125 | push it to an image registry. 126 | 127 | 1. Build the image: 128 | 129 | ```sh 130 | docker build -t quay.io/my-repository/my-plugin:latest . 131 | ``` 132 | 133 | 2. Run the image: 134 | 135 | ```sh 136 | docker run -it --rm -d -p 9001:80 quay.io/my-repository/my-plugin:latest 137 | ``` 138 | 139 | 3. Push the image: 140 | 141 | ```sh 142 | docker push quay.io/my-repository/my-plugin:latest 143 | ``` 144 | 145 | NOTE: If you have a Mac with Apple silicon, you will need to add the flag 146 | `--platform=linux/amd64` when building the image to target the correct platform 147 | to run in-cluster. 148 | 149 | ## Deployment on cluster 150 | 151 | A [Helm](https://helm.sh) chart is available to deploy the plugin to an OpenShift environment. 152 | 153 | The following Helm parameters are required: 154 | 155 | `plugin.image`: The location of the image containing the plugin that was previously pushed 156 | 157 | Additional parameters can be specified if desired. Consult the chart [values](charts/openshift-console-plugin/values.yaml) file for the full set of supported parameters. 158 | 159 | ### Installing the Helm Chart 160 | 161 | Install the chart using the name of the plugin as the Helm release name into a new namespace or an existing namespace as specified by the `plugin_console-plugin-template` parameter and providing the location of the image within the `plugin.image` parameter by using the following command: 162 | 163 | ```shell 164 | helm upgrade -i my-plugin charts/openshift-console-plugin -n my-namespace --create-namespace --set plugin.image=my-plugin-image-location 165 | ``` 166 | 167 | NOTE: When deploying on OpenShift 4.10, it is recommended to add the parameter `--set plugin.securityContext.enabled=false` which will omit configurations related to Pod Security. 168 | 169 | NOTE: When defining i18n namespace, adhere `plugin__` format. The name of the plugin should be extracted from the `consolePlugin` declaration within the [package.json](package.json) file. 170 | 171 | ## i18n 172 | 173 | The plugin template demonstrates how you can translate messages in with [react-i18next](https://react.i18next.com/). The i18n namespace must match 174 | the name of the `ConsolePlugin` resource with the `plugin__` prefix to avoid 175 | naming conflicts. For example, the plugin template uses the 176 | `plugin__console-plugin-template` namespace. You can use the `useTranslation` hook 177 | with this namespace as follows: 178 | 179 | ```tsx 180 | conster Header: React.FC = () => { 181 | const { t } = useTranslation('plugin__console-plugin-template'); 182 | return

{t('Hello, World!')}

; 183 | }; 184 | ``` 185 | 186 | For labels in `console-extensions.json`, you can use the format 187 | `%plugin__console-plugin-template~My Label%`. Console will replace the value with 188 | the message for the current language from the `plugin__console-plugin-template` 189 | namespace. For example: 190 | 191 | ```json 192 | { 193 | "type": "console.navigation/section", 194 | "properties": { 195 | "id": "admin-demo-section", 196 | "perspective": "admin", 197 | "name": "%plugin__console-plugin-template~Plugin Template%" 198 | } 199 | } 200 | ``` 201 | 202 | Running `yarn i18n` updates the JSON files in the `locales` folder of the 203 | plugin template when adding or changing messages. 204 | 205 | ## Linting 206 | 207 | This project adds prettier, eslint, and stylelint. Linting can be run with 208 | `yarn run lint`. 209 | 210 | The stylelint config disallows hex colors since these cause problems with dark 211 | mode (starting in OpenShift console 4.11). You should use the 212 | [PatternFly global CSS variables](https://patternfly-react-main.surge.sh/developer-resources/global-css-variables#global-css-variables) 213 | for colors instead. 214 | 215 | The stylelint config also disallows naked element selectors like `table` and 216 | `.pf-` or `.co-` prefixed classes. This prevents plugins from accidentally 217 | overwriting default console styles, breaking the layout of existing pages. The 218 | best practice is to prefix your CSS classnames with your plugin name to avoid 219 | conflicts. Please don't disable these rules without understanding how they can 220 | break console styles! 221 | 222 | ## Reporting 223 | 224 | Steps to generate reports 225 | 226 | 1. In command prompt, navigate to root folder and execute the command `yarn run cypress-merge` 227 | 2. Then execute command `yarn run cypress-generate` 228 | The cypress-report.html file is generated and should be in (/integration-tests/screenshots) directory 229 | 230 | ## References 231 | 232 | - [Console Plugin SDK README](https://github.com/openshift/console/tree/master/frontend/packages/console-dynamic-plugin-sdk) 233 | - [Customization Plugin Example](https://github.com/spadgett/console-customization-plugin) 234 | - [Dynamic Plugin Enhancement Proposal](https://github.com/openshift/enhancements/blob/master/enhancements/console/dynamic-plugins.md) 235 | -------------------------------------------------------------------------------- /charts/openshift-console-plugin/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /charts/openshift-console-plugin/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: openshift-console-plugin 3 | description: A Helm chart for Kubernetes 4 | type: application 5 | version: 0.1.0 6 | -------------------------------------------------------------------------------- /charts/openshift-console-plugin/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "openshift-console-plugin.name" -}} 5 | {{- default (default .Chart.Name .Release.Name) .Values.plugin.name | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | 9 | {{/* 10 | Create chart name and version as used by the chart label. 11 | */}} 12 | {{- define "openshift-console-plugin.chart" -}} 13 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 14 | {{- end }} 15 | 16 | {{/* 17 | Common labels 18 | */}} 19 | {{- define "openshift-console-plugin.labels" -}} 20 | helm.sh/chart: {{ include "openshift-console-plugin.chart" . }} 21 | {{ include "openshift-console-plugin.selectorLabels" . }} 22 | {{- if .Chart.AppVersion }} 23 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 24 | {{- end }} 25 | app.kubernetes.io/managed-by: {{ .Release.Service }} 26 | {{- end }} 27 | 28 | {{/* 29 | Selector labels 30 | */}} 31 | {{- define "openshift-console-plugin.selectorLabels" -}} 32 | app: {{ include "openshift-console-plugin.name" . }} 33 | app.kubernetes.io/name: {{ include "openshift-console-plugin.name" . }} 34 | app.kubernetes.io/instance: {{ .Release.Name }} 35 | app.kubernetes.io/part-of: {{ include "openshift-console-plugin.name" . }} 36 | {{- end }} 37 | 38 | {{/* 39 | Create the name secret containing the certificate 40 | */}} 41 | {{- define "openshift-console-plugin.certificateSecret" -}} 42 | {{ default (printf "%s-cert" (include "openshift-console-plugin.name" .)) .Values.plugin.certificateSecretName }} 43 | {{- end }} 44 | 45 | {{/* 46 | Create the name of the service account to use 47 | */}} 48 | {{- define "openshift-console-plugin.serviceAccountName" -}} 49 | {{- if .Values.plugin.serviceAccount.create }} 50 | {{- default (include "openshift-console-plugin.name" .) .Values.plugin.serviceAccount.name }} 51 | {{- else }} 52 | {{- default "default" .Values.plugin.serviceAccount.name }} 53 | {{- end }} 54 | {{- end }} 55 | 56 | {{/* 57 | Create the name of the patcher 58 | */}} 59 | {{- define "openshift-console-plugin.patcherName" -}} 60 | {{- printf "%s-patcher" (include "openshift-console-plugin.name" .) }} 61 | {{- end }} 62 | 63 | {{/* 64 | Create the name of the service account to use 65 | */}} 66 | {{- define "openshift-console-plugin.patcherServiceAccountName" -}} 67 | {{- if .Values.plugin.patcherServiceAccount.create }} 68 | {{- default (printf "%s-patcher" (include "openshift-console-plugin.name" .)) .Values.plugin.patcherServiceAccount.name }} 69 | {{- else }} 70 | {{- default "default" .Values.plugin.patcherServiceAccount.name }} 71 | {{- end }} 72 | {{- end }} -------------------------------------------------------------------------------- /charts/openshift-console-plugin/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ template "openshift-console-plugin.name" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{- include "openshift-console-plugin.labels" . | nindent 4 }} 8 | data: 9 | nginx.conf: | 10 | error_log /dev/stdout info; 11 | events {} 12 | http { 13 | access_log /dev/stdout; 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | keepalive_timeout 65; 17 | server { 18 | listen {{ .Values.plugin.port }} ssl; 19 | listen [::]:{{ .Values.plugin.port }} ssl; 20 | ssl_certificate /var/cert/tls.crt; 21 | ssl_certificate_key /var/cert/tls.key; 22 | root /usr/share/nginx/html; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /charts/openshift-console-plugin/templates/consoleplugin.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: console.openshift.io/v1 2 | kind: ConsolePlugin 3 | metadata: 4 | name: {{ template "openshift-console-plugin.name" . }} 5 | labels: 6 | {{- include "openshift-console-plugin.labels" . | nindent 4 }} 7 | spec: 8 | displayName: {{ default (printf "%s Plugin" (include "openshift-console-plugin.name" .)) .Values.plugin.description }} 9 | i18n: 10 | loadType: Preload 11 | backend: 12 | type: Service 13 | service: 14 | name: {{ template "openshift-console-plugin.name" . }} 15 | namespace: {{ .Release.Namespace }} 16 | port: {{ .Values.plugin.port }} 17 | basePath: {{ .Values.plugin.basePath }} 18 | -------------------------------------------------------------------------------- /charts/openshift-console-plugin/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ template "openshift-console-plugin.name" . }} 5 | namespace: {{ .Release.Namespace }} 6 | labels: 7 | {{- include "openshift-console-plugin.labels" . | nindent 4 }} 8 | app.openshift.io/runtime-namespace: {{ .Release.Namespace }} 9 | spec: 10 | replicas: {{ .Values.plugin.replicas }} 11 | selector: 12 | matchLabels: 13 | {{- include "openshift-console-plugin.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | labels: 17 | {{- include "openshift-console-plugin.labels" . | nindent 8 }} 18 | spec: 19 | {{- with .Values.plugin.imagePullSecrets }} 20 | imagePullSecrets: 21 | {{- toYaml . | nindent 8 }} 22 | {{- end }} 23 | containers: 24 | - name: {{ template "openshift-console-plugin.name" . }} 25 | image: {{ required "Plugin image must be specified!" .Values.plugin.image }} 26 | ports: 27 | - containerPort: {{ .Values.plugin.port }} 28 | protocol: TCP 29 | imagePullPolicy: {{ .Values.plugin.imagePullPolicy }} 30 | {{- if and (.Values.plugin.securityContext.enabled) (.Values.plugin.containerSecurityContext) }} 31 | securityContext: {{ tpl (toYaml (omit .Values.plugin.containerSecurityContext "enabled")) $ | nindent 12 }} 32 | {{- end }} 33 | resources: 34 | {{- toYaml .Values.plugin.resources | nindent 12 }} 35 | volumeMounts: 36 | - name: {{ template "openshift-console-plugin.certificateSecret" . }} 37 | readOnly: true 38 | mountPath: /var/cert 39 | - name: nginx-conf 40 | readOnly: true 41 | mountPath: /etc/nginx/nginx.conf 42 | subPath: nginx.conf 43 | volumes: 44 | - name: {{ template "openshift-console-plugin.certificateSecret" . }} 45 | secret: 46 | secretName: {{ template "openshift-console-plugin.certificateSecret" . }} 47 | defaultMode: 420 48 | - name: nginx-conf 49 | configMap: 50 | name: {{ template "openshift-console-plugin.name" . }} 51 | defaultMode: 420 52 | restartPolicy: Always 53 | dnsPolicy: ClusterFirst 54 | {{- if and (.Values.plugin.securityContext.enabled) (.Values.plugin.podSecurityContext) }} 55 | securityContext: {{ tpl (toYaml (omit .Values.plugin.podSecurityContext "enabled")) $ | nindent 8 }} 56 | {{- end }} 57 | strategy: 58 | type: RollingUpdate 59 | rollingUpdate: 60 | maxUnavailable: 25% 61 | maxSurge: 25% 62 | -------------------------------------------------------------------------------- /charts/openshift-console-plugin/templates/patch-consoles-job.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.plugin.jobs.patchConsoles.enabled }} 2 | apiVersion: batch/v1 3 | kind: Job 4 | metadata: 5 | name: {{ template "openshift-console-plugin.patcherName" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "openshift-console-plugin.labels" . | nindent 4 }} 9 | annotations: 10 | helm.sh/hook: post-install,post-upgrade 11 | helm.sh/hook-delete-policy: before-hook-creation 12 | spec: 13 | parallelism: 1 14 | template: 15 | metadata: 16 | labels: 17 | {{- include "openshift-console-plugin.labels" . | nindent 8 }} 18 | spec: 19 | restartPolicy: OnFailure 20 | serviceAccountName: {{ template "openshift-console-plugin.patcherServiceAccountName" . }} 21 | {{- if and (.Values.plugin.securityContext.enabled) (.Values.plugin.jobs.patchConsoles.podSecurityContext.enabled) }} 22 | securityContext: {{ tpl (toYaml (omit .Values.plugin.jobs.patchConsoles.podSecurityContext "enabled")) $ | nindent 8 }} 23 | {{- end }} 24 | terminationGracePeriodSeconds: 400 25 | dnsPolicy: ClusterFirst 26 | containers: 27 | - name: {{ template "openshift-console-plugin.patcherName" . }} 28 | image: {{ required "Patcher image must be specified!" .Values.plugin.jobs.patchConsoles.image }} 29 | {{- if and (.Values.plugin.securityContext.enabled) (.Values.plugin.containerSecurityContext) }} 30 | securityContext: {{ tpl (toYaml (omit .Values.plugin.jobs.patchConsoles.containerSecurityContext "enabled")) $ | nindent 12 }} 31 | {{- end }} 32 | resources: 33 | {{- toYaml .Values.plugin.jobs.patchConsoles.resources | nindent 12 }} 34 | command: 35 | - /bin/bash 36 | - -c 37 | - | 38 | existingPlugins=$(oc get consoles.operator.openshift.io cluster -o json | jq -c '.spec.plugins // []') 39 | mergedPlugins=$(jq --argjson existingPlugins "${existingPlugins}" --argjson consolePlugin '["{{ template "openshift-console-plugin.name" . }}"]' -c -n '$existingPlugins + $consolePlugin | unique') 40 | patchedPlugins=$(jq --argjson mergedPlugins $mergedPlugins -n -c '{ "spec": { "plugins": $mergedPlugins } }') 41 | oc patch consoles.operator.openshift.io cluster --patch $patchedPlugins --type=merge 42 | {{- end }} -------------------------------------------------------------------------------- /charts/openshift-console-plugin/templates/patcher-clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.plugin.jobs.patchConsoles.enabled }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ template "openshift-console-plugin.patcherName" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "openshift-console-plugin.labels" . | nindent 4 }} 9 | rules: 10 | - apiGroups: ["operator.openshift.io"] 11 | resources: ["consoles"] 12 | verbs: ["get","list","patch", "update"] 13 | {{- end }} -------------------------------------------------------------------------------- /charts/openshift-console-plugin/templates/patcher-clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.plugin.jobs.patchConsoles.enabled }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: {{ template "openshift-console-plugin.patcherName" . }} 6 | namespace: {{ .Release.Namespace }} 7 | labels: 8 | {{- include "openshift-console-plugin.labels" . | nindent 4 }} 9 | roleRef: 10 | apiGroup: rbac.authorization.k8s.io 11 | kind: ClusterRole 12 | name: {{ template "openshift-console-plugin.patcherName" . }} 13 | subjects: 14 | - kind: ServiceAccount 15 | name: {{ template "openshift-console-plugin.patcherServiceAccountName" . }} 16 | namespace: {{ .Release.Namespace }} 17 | {{- end }} -------------------------------------------------------------------------------- /charts/openshift-console-plugin/templates/patcher-serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if and (.Values.plugin.patcherServiceAccount.create) (.Values.plugin.jobs.patchConsoles.enabled) -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "openshift-console-plugin.patcherServiceAccountName" . }} 6 | labels: 7 | {{- include "openshift-console-plugin.labels" . | nindent 4 }} 8 | {{- with .Values.plugin.patcherServiceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /charts/openshift-console-plugin/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | {{- if not .Values.certificateSecretName }} 5 | annotations: 6 | service.alpha.openshift.io/serving-cert-secret-name: {{ template "openshift-console-plugin.certificateSecret" . }} 7 | {{- end }} 8 | name: {{ template "openshift-console-plugin.name" . }} 9 | namespace: {{ .Release.Namespace }} 10 | labels: 11 | {{- include "openshift-console-plugin.labels" . | nindent 4 }} 12 | spec: 13 | ports: 14 | - name: {{ .Values.plugin.port }}-tcp 15 | protocol: TCP 16 | port: {{ .Values.plugin.port }} 17 | targetPort: {{ .Values.plugin.port }} 18 | selector: 19 | {{- include "openshift-console-plugin.selectorLabels" . | nindent 4 }} 20 | type: ClusterIP 21 | sessionAffinity: None 22 | -------------------------------------------------------------------------------- /charts/openshift-console-plugin/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.plugin.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "openshift-console-plugin.serviceAccountName" . }} 6 | labels: 7 | {{- include "openshift-console-plugin.labels" . | nindent 4 }} 8 | {{- with .Values.plugin.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /charts/openshift-console-plugin/values.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | plugin: 3 | name: "" 4 | description: "" 5 | image: "" 6 | imagePullPolicy: IfNotPresent 7 | imagePullSecrets: [] 8 | replicas: 2 9 | port: 9443 10 | securityContext: 11 | enabled: true 12 | podSecurityContext: 13 | enabled: true 14 | runAsNonRoot: true 15 | seccompProfile: 16 | type: RuntimeDefault 17 | containerSecurityContext: 18 | enabled: true 19 | allowPrivilegeEscalation: false 20 | capabilities: 21 | drop: 22 | - ALL 23 | resources: 24 | requests: 25 | cpu: 10m 26 | memory: 50Mi 27 | basePath: / 28 | certificateSecretName: "" 29 | serviceAccount: 30 | create: true 31 | annotations: {} 32 | name: "" 33 | patcherServiceAccount: 34 | create: true 35 | annotations: {} 36 | name: "" 37 | jobs: 38 | patchConsoles: 39 | enabled: true 40 | image: "registry.redhat.io/openshift4/ose-tools-rhel9@sha256:ee65b244fc94d5765514c604dc0a288517dcdce68de65128d047622b6b30a618" 41 | podSecurityContext: 42 | enabled: true 43 | runAsNonRoot: true 44 | seccompProfile: 45 | type: RuntimeDefault 46 | containerSecurityContext: 47 | enabled: true 48 | allowPrivilegeEscalation: false 49 | capabilities: 50 | drop: 51 | - ALL 52 | resources: 53 | requests: 54 | cpu: 10m 55 | memory: 50Mi 56 | -------------------------------------------------------------------------------- /console-extensions.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "console.page/route", 4 | "properties": { 5 | "exact": true, 6 | "path": "/example", 7 | "component": { "$codeRef": "ExamplePage" } 8 | } 9 | }, 10 | { 11 | "type": "console.navigation/href", 12 | "properties": { 13 | "id": "example", 14 | "name": "%plugin__console-plugin-template~Plugin Example%", 15 | "href": "/example", 16 | "perspective": "admin", 17 | "section": "home" 18 | } 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /ct.yaml: -------------------------------------------------------------------------------- 1 | chart-dirs: 2 | - charts 3 | validate-maintainers: false 4 | remote: origin 5 | target-branch: main 6 | -------------------------------------------------------------------------------- /i18n-scripts/build-i18n.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -exuo pipefail 4 | 5 | FILE_PATTERN="{!(dist|node_modules)/**/*.{js,jsx,ts,tsx,json},*.{js,jsx,ts,tsx,json}}" 6 | 7 | i18next "${FILE_PATTERN}" [-oc] -c "./i18next-parser.config.js" -o "locales/\$LOCALE/\$NAMESPACE.json" 8 | -------------------------------------------------------------------------------- /i18n-scripts/common.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | isDirectory(filePath) { 6 | try { 7 | const stat = fs.lstatSync(filePath); 8 | return stat.isDirectory(); 9 | } catch (e) { 10 | // lstatSync throws an error if path doesn't exist 11 | return false; 12 | } 13 | }, 14 | parseFolder(directory, argFunction, packageDir) { 15 | (async () => { 16 | try { 17 | const files = await fs.promises.readdir(directory); 18 | for (const file of files) { 19 | const filePath = path.join(directory, file); 20 | argFunction(filePath, packageDir); 21 | } 22 | } catch (e) { 23 | console.error(`Failed to parseFolder ${directory}:`, e); 24 | } 25 | })(); 26 | }, 27 | deleteFile(filePath) { 28 | try { 29 | fs.unlinkSync(filePath); 30 | } catch (e) { 31 | console.error(`Failed to delete file ${filePath}:`, e); 32 | } 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /i18n-scripts/lexers.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events'); 2 | const jsonc = require('comment-json'); 3 | 4 | /** 5 | * Custom JSON parser for localizing keys matching format: /%.+%/ 6 | */ 7 | module.exports.CustomJSONLexer = class extends EventEmitter { 8 | extract(content, filename) { 9 | let keys = []; 10 | console.log(1) 11 | try { 12 | jsonc.parse( 13 | content, 14 | (key, value) => { 15 | if (typeof value === 'string') { 16 | const match = value.match(/^%(.+)%$/); 17 | if (match && match[1]) { 18 | keys.push({ key: match[1] }); 19 | } 20 | } 21 | return value; 22 | }, 23 | true, 24 | ); 25 | } catch (e) { 26 | console.error('Failed to parse as JSON.', filename, e); 27 | keys = []; 28 | } 29 | return keys; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /i18n-scripts/set-english-defaults.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const pluralize = require('pluralize'); 4 | const common = require('./common.js'); 5 | 6 | const publicDir = path.join(__dirname, './../locales/'); 7 | 8 | function determineRule(key) { 9 | if (key.includes('WithCount_plural')) { 10 | return 0; 11 | } 12 | if (key.includes('WithCount')) { 13 | return 1; 14 | } 15 | if (key.includes('_plural')) { 16 | return 2; 17 | } 18 | return 3; 19 | } 20 | 21 | function updateFile(fileName) { 22 | const file = require(fileName); 23 | const updatedFile = {}; 24 | 25 | const keys = Object.keys(file); 26 | 27 | let originalKey; 28 | 29 | for (let i = 0; i < keys.length; i++) { 30 | if (file[keys[i]] === '') { 31 | // follow i18next rules 32 | // "key": "item", 33 | // "key_plural": "items", 34 | // "keyWithCount": "{{count}} item", 35 | // "keyWithCount_plural": "{{count}} items" 36 | switch (determineRule(keys[i])) { 37 | case 0: 38 | [originalKey] = keys[i].split('WithCount_plural'); 39 | updatedFile[keys[i]] = `{{count}} ${pluralize(originalKey)}`; 40 | break; 41 | case 1: 42 | [originalKey] = keys[i].split('WithCount'); 43 | updatedFile[keys[i]] = `{{count}} ${originalKey}`; 44 | break; 45 | case 2: 46 | [originalKey] = keys[i].split('_plural'); 47 | updatedFile[keys[i]] = pluralize(originalKey); 48 | break; 49 | default: 50 | updatedFile[keys[i]] = keys[i]; 51 | } 52 | } else { 53 | updatedFile[keys[i]] = file[keys[i]]; 54 | } 55 | } 56 | 57 | fs.promises 58 | .writeFile(fileName, JSON.stringify(updatedFile, null, 2)) 59 | .catch((e) => console.error(fileName, e)); 60 | } 61 | 62 | function processLocalesFolder(filePath) { 63 | if (path.basename(filePath) === 'en') { 64 | common.parseFolder(filePath, updateFile); 65 | } 66 | } 67 | 68 | common.parseFolder(publicDir, processLocalesFolder); 69 | -------------------------------------------------------------------------------- /i18next-parser.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires, no-undef 2 | const { CustomJSONLexer } = require('./i18n-scripts/lexers'); 3 | 4 | // eslint-disable-next-line no-undef 5 | module.exports = { 6 | sort: true, 7 | createOldCatalogs: false, 8 | keySeparator: false, 9 | locales: ['en'], 10 | namespaceSeparator: '~', 11 | reactNamespace: false, 12 | defaultNamespace: 'plugin__console-plugin-template', 13 | useKeysAsDefaultValue: true, 14 | 15 | // see below for more details 16 | lexers: { 17 | hbs: ['HandlebarsLexer'], 18 | handlebars: ['HandlebarsLexer'], 19 | 20 | htm: ['HTMLLexer'], 21 | html: ['HTMLLexer'], 22 | 23 | mjs: ['JavascriptLexer'], 24 | js: ['JavascriptLexer'], // if you're writing jsx inside .js files, change this to JsxLexer 25 | ts: ['JavascriptLexer'], 26 | jsx: ['JsxLexer'], 27 | tsx: ['JsxLexer'], 28 | json: [CustomJSONLexer], 29 | 30 | default: ['JavascriptLexer'], 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /install_helm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | USE_SUDO="false" 4 | HELM_INSTALL_DIR="/tmp" 5 | 6 | curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 7 | chmod 700 get_helm.sh 8 | source get_helm.sh 9 | 10 | rm -rf get_helm.sh -------------------------------------------------------------------------------- /integration-tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "cypress/globals": true, 4 | "node": true 5 | }, 6 | "extends": ["../.eslintrc.yml", "plugin:cypress/recommended"], 7 | "plugins": ["cypress"], 8 | "rules": { 9 | "no-console": "off", 10 | "no-namespace": "off", 11 | "no-redeclare": "off", 12 | "promise/catch-or-return": "off", 13 | "promise/no-nesting": "off", 14 | "@typescript-eslint/no-var-requires":"off", 15 | "@typescript-eslint/no-namespace":"off" 16 | } 17 | } -------------------------------------------------------------------------------- /integration-tests/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('cypress'); 2 | 3 | module.exports = defineConfig({ 4 | viewportWidth: 1920, 5 | viewportHeight: 1080, 6 | screenshotsFolder: './screenshots', 7 | videosFolder: './videos', 8 | video: true, 9 | reporter: '../../node_modules/cypress-multi-reporters', 10 | reporterOptions: { 11 | configFile: 'reporter-config.json', 12 | }, 13 | fixturesFolder: 'fixtures', 14 | defaultCommandTimeout: 30000, 15 | retries: { 16 | runMode: 1, 17 | openMode: 0, 18 | }, 19 | e2e: { 20 | setupNodeEvents(on, config) { 21 | return require('./plugins/index.ts')(on, config); 22 | }, 23 | specPattern: 'tests/**/*.cy.{js,jsx,ts,tsx}', 24 | supportFile: 'support/index.ts', 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /integration-tests/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /integration-tests/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import * as wp from '@cypress/webpack-preprocessor'; 2 | 3 | module.exports = (on, config) => { 4 | const options = { 5 | webpackOptions: { 6 | resolve: { 7 | extensions: ['.ts', '.tsx', '.js'], 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.tsx?$/, 13 | loader: 'ts-loader', 14 | options: { happyPackMode: true, transpileOnly: true }, 15 | }, 16 | ], 17 | }, 18 | }, 19 | }; 20 | on('file:preprocessor', wp(options)); 21 | // `config` is the resolved Cypress config 22 | config.baseUrl = `${process.env.BRIDGE_BASE_ADDRESS || 'http://localhost:9000/'}`; 23 | config.env.BRIDGE_KUBEADMIN_PASSWORD = process.env.BRIDGE_KUBEADMIN_PASSWORD; 24 | return config; 25 | }; 26 | -------------------------------------------------------------------------------- /integration-tests/reporter-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "mocha-junit-reporter, mochawesome", 3 | "mochaJunitReporterReporterOptions": { 4 | "mochaFile": "./screenshots/junit_cypress-[hash].xml", 5 | "toConsole": false 6 | }, 7 | "mochawesomeReporterOptions": { 8 | "reportDir": "./screenshots/", 9 | "reportFilename": "cypress_report", 10 | "overwrite": false, 11 | "html": false, 12 | "json": true 13 | } 14 | } -------------------------------------------------------------------------------- /integration-tests/support/index.ts: -------------------------------------------------------------------------------- 1 | // Import commands.js using ES2015 syntax: 2 | import './login'; 3 | 4 | export const checkErrors = () => 5 | cy.window().then((win) => { 6 | assert.isTrue(!win.windowError, win.windowError); 7 | }); 8 | -------------------------------------------------------------------------------- /integration-tests/support/login.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace Cypress { 3 | interface Chainable { 4 | login(username?: string, password?: string): Chainable; 5 | logout(): Chainable; 6 | } 7 | } 8 | } 9 | 10 | const KUBEADMIN_USERNAME = 'kubeadmin'; 11 | 12 | // This will add 'cy.login(...)' 13 | // ex: cy.login('my-user', 'my-password') 14 | Cypress.Commands.add('login', (username: string, password: string) => { 15 | // Check if auth is disabled (for a local development environment). 16 | cy.visit('/'); // visits baseUrl which is set in plugins/index.js 17 | cy.window().then((win) => { 18 | if (win.SERVER_FLAGS?.authDisabled) { 19 | return; 20 | } 21 | 22 | // Make sure we clear the cookie in case a previous test failed to logout. 23 | cy.clearCookie('openshift-session-token'); 24 | 25 | cy.get('#inputUsername').type(username || KUBEADMIN_USERNAME); 26 | cy.get('#inputPassword').type(password || Cypress.env('BRIDGE_KUBEADMIN_PASSWORD')); 27 | cy.get('button[type=submit]').click(); 28 | 29 | cy.get('[data-test="username"]').should('be.visible'); 30 | }); 31 | }); 32 | 33 | Cypress.Commands.add('logout', () => { 34 | // Check if auth is disabled (for a local development environment). 35 | cy.window().then((win) => { 36 | if (win.SERVER_FLAGS?.authDisabled) { 37 | return; 38 | } 39 | cy.get('[data-test="username"]').click(); 40 | cy.get('[data-test="log-out"]').should('be.visible'); 41 | cy.get('[data-test="log-out"]').click({ force: true }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /integration-tests/tests/example-page.cy.ts: -------------------------------------------------------------------------------- 1 | import { checkErrors } from '../support'; 2 | 3 | const PLUGIN_TEMPLATE_NAME = 'console-plugin-template'; 4 | const PLUGIN_TEMPLATE_PULL_SPEC = Cypress.env('PLUGIN_TEMPLATE_PULL_SPEC'); 5 | export const isLocalDevEnvironment = Cypress.config('baseUrl').includes('localhost'); 6 | 7 | export const guidedTour = { 8 | close: () => { 9 | cy.get('body').then(($body) => { 10 | if ($body.find(`[data-test="guided-tour-modal"]`).length > 0) { 11 | cy.get(`[data-test="tour-step-footer-secondary"]`).contains('Skip tour').click(); 12 | } 13 | }); 14 | }, 15 | isOpen: () => { 16 | cy.get('body').then(($body) => { 17 | if ($body.find(`[data-test="guided-tour-modal"]`).length > 0) { 18 | cy.get(`[data-test="guided-tour-modal"]`).should('be.visible'); 19 | } 20 | }); 21 | }, 22 | }; 23 | 24 | const installHelmChart = (path: string) => { 25 | cy.exec( 26 | `cd ../../console-plugin-template && ${path} upgrade -i ${PLUGIN_TEMPLATE_NAME} charts/openshift-console-plugin -n ${PLUGIN_TEMPLATE_NAME} --create-namespace --set plugin.image=${PLUGIN_TEMPLATE_PULL_SPEC}`, 27 | { 28 | failOnNonZeroExit: false, 29 | }, 30 | ) 31 | .get('[data-test="refresh-web-console"]', { timeout: 300000 }) 32 | .should('exist') 33 | .then((result) => { 34 | cy.reload(); 35 | cy.visit(`/dashboards`); 36 | cy.log('Error installing helm chart: ', result.stderr); 37 | cy.log('Successfully installed helm chart: ', result.stdout); 38 | }); 39 | }; 40 | const deleteHelmChart = (path: string) => { 41 | cy.exec( 42 | `cd ../../console-plugin-template && ${path} uninstall ${PLUGIN_TEMPLATE_NAME} -n ${PLUGIN_TEMPLATE_NAME} && oc delete namespaces ${PLUGIN_TEMPLATE_NAME}`, 43 | { 44 | failOnNonZeroExit: false, 45 | }, 46 | ).then((result) => { 47 | cy.log('Error uninstalling helm chart: ', result.stderr); 48 | cy.log('Successfully uninstalled helm chart: ', result.stdout); 49 | }); 50 | }; 51 | 52 | describe('Console plugin template test', () => { 53 | before(() => { 54 | cy.login(); 55 | guidedTour.isOpen(); 56 | guidedTour.close(); 57 | if (!isLocalDevEnvironment) { 58 | console.log('this is not a local env, installig helm'); 59 | 60 | cy.exec('cd ../../console-plugin-template && ./install_helm.sh', { 61 | failOnNonZeroExit: false, 62 | }).then((result) => { 63 | cy.log('Error installing helm binary: ', result.stderr); 64 | cy.log('Successfully installed helm binary in "/tmp" directory: ', result.stdout); 65 | 66 | installHelmChart('/tmp/helm'); 67 | }); 68 | } else { 69 | console.log('this is a local env, not installing helm'); 70 | 71 | installHelmChart('helm'); 72 | } 73 | }); 74 | 75 | afterEach(() => { 76 | checkErrors(); 77 | }); 78 | 79 | after(() => { 80 | if (!isLocalDevEnvironment) { 81 | deleteHelmChart('/tmp/helm'); 82 | } else { 83 | deleteHelmChart('helm'); 84 | } 85 | cy.logout(); 86 | }); 87 | 88 | it('Verify the example page title', () => { 89 | cy.get('[data-quickstart-id="qs-nav-home"]').click(); 90 | cy.get('[data-test="nav"]').contains('Plugin Example').click(); 91 | cy.url().should('include', '/example'); 92 | cy.get('[data-test="example-page-title"]').should('contain', 'Hello, Plugin!'); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /integration-tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "types":["cypress","node"], 6 | "isolatedModules": false 7 | }, 8 | "include": ["../node_modules/cypress", "./**/*.ts"] 9 | } -------------------------------------------------------------------------------- /locales/en/plugin__console-plugin-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "After cloning this project, replace references to": "After cloning this project, replace references to", 3 | "and other plugin metadata in package.json with values for your plugin.": "and other plugin metadata in package.json with values for your plugin.", 4 | "console-template-plugin": "console-template-plugin", 5 | "exposedModules": "exposedModules", 6 | "Hello, Plugin!": "Hello, Plugin!", 7 | "in package.json mapping the reference to the module.": "in package.json mapping the reference to the module.", 8 | "Plugin Example": "Plugin Example", 9 | "Success!": "Success!", 10 | "This is a custom page contributed by the console plugin template. The extension that adds the page is declared in console-extensions.json in the project root along with the corresponding nav item. Update console-extensions.json to change or add extensions. Code references in console-extensions.json must have a corresponding property": "This is a custom page contributed by the console plugin template. The extension that adds the page is declared in console-extensions.json in the project root along with the corresponding nav item. Update console-extensions.json to change or add extensions. Code references in console-extensions.json must have a corresponding property", 11 | "Your plugin is working.": "Your plugin is working." 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "console-plugin-template", 3 | "version": "0.0.1", 4 | "description": "Template project for OpenShift Console plugins", 5 | "private": true, 6 | "license": "Apache-2.0", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/openshift/console-plugin-template.git" 10 | }, 11 | "scripts": { 12 | "clean": "rm -rf dist", 13 | "build": "yarn clean && NODE_ENV=production yarn webpack", 14 | "build-dev": "yarn clean && yarn webpack", 15 | "start": "yarn webpack serve --progress", 16 | "start-console": "./start-console.sh", 17 | "i18n": "./i18n-scripts/build-i18n.sh && node ./i18n-scripts/set-english-defaults.js", 18 | "lint": "yarn eslint src integration-tests --fix && stylelint 'src/**/*.css' --allow-empty-input --fix", 19 | "test-cypress": "cd integration-tests && cypress open", 20 | "test-cypress-headless": "cd integration-tests && node --max-old-space-size=4096 ../node_modules/.bin/cypress run --browser ${BRIDGE_E2E_BROWSER_NAME:=electron}", 21 | "cypress-merge": "mochawesome-merge ./integration-tests/screenshots/cypress_report*.json > ./integration-tests/screenshots/cypress.json", 22 | "cypress-generate": "marge -o ./integration-tests/screenshots/ -f cypress-report -t 'OpenShift Console Plugin Template Cypress Test Results' -p 'OpenShift Cypress Plugin Template Test Results' --showPassed false --assetsDir ./integration-tests/screenshots/cypress/assets ./integration-tests/screenshots/cypress.json", 23 | "cypress-postreport": "yarn cypress-merge && yarn cypress-generate", 24 | "webpack": "node -r ts-node/register ./node_modules/.bin/webpack" 25 | }, 26 | "devDependencies": { 27 | "@cypress/webpack-preprocessor": "^5.15.5", 28 | "@openshift-console/dynamic-plugin-sdk": "1.4.0", 29 | "@openshift-console/dynamic-plugin-sdk-webpack": "1.1.1", 30 | "@patternfly/react-core": "^6.2.2", 31 | "@patternfly/react-icons": "^6.2.2", 32 | "@patternfly/react-table": "^6.2.2", 33 | "@types/node": "^18.0.0", 34 | "@types/react": "^17.0.37", 35 | "@types/react-helmet": "^6.1.4", 36 | "@types/react-router-dom": "^5.3.2", 37 | "@typescript-eslint/eslint-plugin": "^5.14.0", 38 | "@typescript-eslint/parser": "^5.14.0", 39 | "copy-webpack-plugin": "^6.4.1", 40 | "css-loader": "^6.7.1", 41 | "cypress": "^12.17.4", 42 | "cypress-multi-reporters": "^1.6.2", 43 | "eslint": "^8.10.0", 44 | "eslint-config-prettier": "^8.5.0", 45 | "eslint-plugin-cypress": "^2.12.1", 46 | "eslint-plugin-prettier": "^4.0.0", 47 | "eslint-plugin-react": "^7.29.1", 48 | "i18next-parser": "^3.11.0", 49 | "mocha-junit-reporter": "^2.2.0", 50 | "mochawesome": "^7.1.3", 51 | "mochawesome-merge": "^4.3.0", 52 | "pluralize": "^8.0.0", 53 | "prettier": "^2.7.1", 54 | "prettier-stylelint": "^0.4.2", 55 | "react": "^17.0.1", 56 | "react-dom": "^17.0.1", 57 | "react-helmet": "^6.1.0", 58 | "react-i18next": "^11.7.3", 59 | "react-router": "5.3.x", 60 | "react-router-dom": "5.3.x", 61 | "style-loader": "^3.3.1", 62 | "stylelint": "^15.3.0", 63 | "stylelint-config-standard": "^31.0.0", 64 | "ts-loader": "^9.3.1", 65 | "ts-node": "^10.8.1", 66 | "typescript": "^4.7.4", 67 | "webpack": "5.75.0", 68 | "webpack-cli": "^4.9.2", 69 | "webpack-dev-server": "^4.7.4" 70 | }, 71 | "consolePlugin": { 72 | "name": "console-plugin-template", 73 | "version": "0.0.1", 74 | "displayName": "OpenShift Console Plugin Template", 75 | "description": "Template project for OpenShift Console plugins. Edit package.json to change this message and the plugin name.", 76 | "exposedModules": { 77 | "ExamplePage": "./components/ExamplePage" 78 | }, 79 | "dependencies": { 80 | "@console/pluginAPI": "*" 81 | } 82 | }, 83 | "peerDependencies": { 84 | "@babel/core": "^7.0.1", 85 | "@babel/preset-env": "^7.0.0", 86 | "babel-loader": "^8.0.2", 87 | "i18next": "^23.11.5", 88 | "mocha": "^10.5.1" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/components/ExamplePage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { Content, PageSection, Title } from '@patternfly/react-core'; 5 | import { CheckCircleIcon } from '@patternfly/react-icons'; 6 | import './example.css'; 7 | 8 | export default function ExamplePage() { 9 | const { t } = useTranslation('plugin__console-plugin-template'); 10 | 11 | return ( 12 | <> 13 | 14 | {t('Hello, Plugin!')} 15 | 16 | 17 | {t('Hello, Plugin!')} 18 | 19 | 20 | 21 | 22 | {t('Success!')} 23 | {' '} 24 | {t('Your plugin is working.')} 25 | 26 | 27 | {t( 28 | 'This is a custom page contributed by the console plugin template. The extension that adds the page is declared in console-extensions.json in the project root along with the corresponding nav item. Update console-extensions.json to change or add extensions. Code references in console-extensions.json must have a corresponding property', 29 | )} 30 | {t('exposedModules')}{' '} 31 | {t('in package.json mapping the reference to the module.')} 32 | 33 | 34 | {t('After cloning this project, replace references to')}{' '} 35 | {t('console-template-plugin')}{' '} 36 | {t('and other plugin metadata in package.json with values for your plugin.')} 37 | 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/example.css: -------------------------------------------------------------------------------- 1 | /* Prefixing your CSS classes with your plugin name is a best practice to avoid 2 | * collisions with other plugin styles. */ 3 | .console-plugin-template__nice { 4 | /* Use PF global vars for colors to support dark mode in OpenShift 4.11. 5 | * https://patternfly-react-main.surge.sh/developer-resources/global-css-variables */ 6 | color: var(--pf-global--palette--blue-400); 7 | } 8 | -------------------------------------------------------------------------------- /start-console.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | CONSOLE_IMAGE=${CONSOLE_IMAGE:="quay.io/openshift/origin-console:latest"} 6 | CONSOLE_PORT=${CONSOLE_PORT:=9000} 7 | CONSOLE_IMAGE_PLATFORM=${CONSOLE_IMAGE_PLATFORM:="linux/amd64"} 8 | 9 | # Plugin metadata is declared in package.json 10 | PLUGIN_NAME=${npm_package_consolePlugin_name} 11 | 12 | echo "Starting local OpenShift console..." 13 | 14 | BRIDGE_USER_AUTH="disabled" 15 | BRIDGE_K8S_MODE="off-cluster" 16 | BRIDGE_K8S_AUTH="bearer-token" 17 | BRIDGE_K8S_MODE_OFF_CLUSTER_SKIP_VERIFY_TLS=true 18 | BRIDGE_K8S_MODE_OFF_CLUSTER_ENDPOINT=$(oc whoami --show-server) 19 | # The monitoring operator is not always installed (e.g. for local OpenShift). Tolerate missing config maps. 20 | set +e 21 | BRIDGE_K8S_MODE_OFF_CLUSTER_THANOS=$(oc -n openshift-config-managed get configmap monitoring-shared-config -o jsonpath='{.data.thanosPublicURL}' 2>/dev/null) 22 | BRIDGE_K8S_MODE_OFF_CLUSTER_ALERTMANAGER=$(oc -n openshift-config-managed get configmap monitoring-shared-config -o jsonpath='{.data.alertmanagerPublicURL}' 2>/dev/null) 23 | set -e 24 | BRIDGE_K8S_AUTH_BEARER_TOKEN=$(oc whoami --show-token 2>/dev/null) 25 | BRIDGE_USER_SETTINGS_LOCATION="localstorage" 26 | BRIDGE_I18N_NAMESPACES="plugin__${PLUGIN_NAME}" 27 | 28 | # Don't fail if the cluster doesn't have gitops. 29 | set +e 30 | GITOPS_HOSTNAME=$(oc -n openshift-gitops get route cluster -o jsonpath='{.spec.host}' 2>/dev/null) 31 | set -e 32 | if [ -n "$GITOPS_HOSTNAME" ]; then 33 | BRIDGE_K8S_MODE_OFF_CLUSTER_GITOPS="https://$GITOPS_HOSTNAME" 34 | fi 35 | 36 | echo "API Server: $BRIDGE_K8S_MODE_OFF_CLUSTER_ENDPOINT" 37 | echo "Console Image: $CONSOLE_IMAGE" 38 | echo "Console URL: http://localhost:${CONSOLE_PORT}" 39 | echo "Console Platform: $CONSOLE_IMAGE_PLATFORM" 40 | 41 | # Prefer podman if installed. Otherwise, fall back to docker. 42 | if [ -x "$(command -v podman)" ]; then 43 | if [ "$(uname -s)" = "Linux" ]; then 44 | # Use host networking on Linux since host.containers.internal is unreachable in some environments. 45 | BRIDGE_PLUGINS="${PLUGIN_NAME}=http://localhost:9001" 46 | podman run --pull always --platform $CONSOLE_IMAGE_PLATFORM --rm --network=host --env-file <(set | grep BRIDGE) $CONSOLE_IMAGE 47 | else 48 | BRIDGE_PLUGINS="${PLUGIN_NAME}=http://host.containers.internal:9001" 49 | podman run --pull always --platform $CONSOLE_IMAGE_PLATFORM --rm -p "$CONSOLE_PORT":9000 --env-file <(set | grep BRIDGE) $CONSOLE_IMAGE 50 | fi 51 | else 52 | BRIDGE_PLUGINS="${PLUGIN_NAME}=http://host.docker.internal:9001" 53 | docker run --pull always --platform $CONSOLE_IMAGE_PLATFORM --rm -p "$CONSOLE_PORT":9000 --env-file <(set | grep BRIDGE) $CONSOLE_IMAGE 54 | fi 55 | -------------------------------------------------------------------------------- /test-frontend.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # https://ci-operator-configresolver-ui-ci.apps.ci.l2s4.p1.openshiftapps.com/help#env 6 | OPENSHIFT_CI=${OPENSHIFT_CI:=false} 7 | ARTIFACT_DIR=${ARTIFACT_DIR:=/tmp/artifacts} 8 | 9 | yarn i18n 10 | GIT_STATUS="$(git status --short --untracked-files -- locales)" 11 | if [ -n "$GIT_STATUS" ]; then 12 | echo "i18n files are not up to date. Run 'yarn i18n' then commit changes." 13 | git --no-pager diff 14 | exit 1 15 | fi -------------------------------------------------------------------------------- /test-prow-e2e.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -exuo pipefail 4 | 5 | ARTIFACT_DIR=${ARTIFACT_DIR:=/tmp/artifacts} 6 | SCREENSHOTS_DIR=integration-tests/screenshots 7 | INSTALLER_DIR=${INSTALLER_DIR:=${ARTIFACT_DIR}/installer} 8 | 9 | function copyArtifacts { 10 | if [ -d "$ARTIFACT_DIR" ] && [ -d "$SCREENSHOTS_DIR" ]; then 11 | if [[ -z "$(ls -A -- "$SCREENSHOTS_DIR")" ]]; then 12 | echo "No artifacts were copied." 13 | else 14 | echo "Copying artifacts from $(pwd)..." 15 | cp -r "$SCREENSHOTS_DIR" "${ARTIFACT_DIR}/screenshots" 16 | fi 17 | fi 18 | } 19 | 20 | trap copyArtifacts EXIT 21 | 22 | 23 | # don't log kubeadmin-password 24 | set +x 25 | BRIDGE_KUBEADMIN_PASSWORD="$(cat "${KUBEADMIN_PASSWORD_FILE:-${INSTALLER_DIR}/auth/kubeadmin-password}")" 26 | export BRIDGE_KUBEADMIN_PASSWORD 27 | set -x 28 | BRIDGE_BASE_ADDRESS="$(oc get consoles.config.openshift.io cluster -o jsonpath='{.status.consoleURL}')" 29 | export BRIDGE_BASE_ADDRESS 30 | 31 | echo "Install dependencies" 32 | if [ ! -d node_modules ]; then 33 | yarn install 34 | fi 35 | 36 | echo "Runs Cypress tests in headless mode" 37 | yarn run test-cypress-headless -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "target": "es2020", 7 | "sourceMap": true, 8 | "jsx": "react", 9 | "allowJs": true, 10 | "strict": false, 11 | "noUnusedLocals": true 12 | }, 13 | "include": ["src"], 14 | "exclude": ["node_modules"], 15 | "ts-node": { 16 | "files": true, 17 | "transpileOnly": true, 18 | "compilerOptions": { 19 | "module": "commonjs" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | import * as path from 'path'; 4 | import { Configuration as WebpackConfiguration } from 'webpack'; 5 | import { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server'; 6 | import { ConsoleRemotePlugin } from '@openshift-console/dynamic-plugin-sdk-webpack'; 7 | 8 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 9 | 10 | const isProd = process.env.NODE_ENV === 'production'; 11 | 12 | interface Configuration extends WebpackConfiguration { 13 | devServer?: WebpackDevServerConfiguration; 14 | } 15 | 16 | const config: Configuration = { 17 | mode: isProd ? 'production' : 'development', 18 | // No regular entry points needed. All plugin related scripts are generated via ConsoleRemotePlugin. 19 | entry: {}, 20 | context: path.resolve(__dirname, 'src'), 21 | output: { 22 | path: path.resolve(__dirname, 'dist'), 23 | filename: isProd ? '[name]-bundle-[hash].min.js' : '[name]-bundle.js', 24 | chunkFilename: isProd ? '[name]-chunk-[chunkhash].min.js' : '[name]-chunk.js', 25 | }, 26 | resolve: { 27 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.(jsx?|tsx?)$/, 33 | exclude: /\/node_modules\//, 34 | use: [ 35 | { 36 | loader: 'ts-loader', 37 | options: { 38 | configFile: path.resolve(__dirname, 'tsconfig.json'), 39 | }, 40 | }, 41 | ], 42 | }, 43 | { 44 | test: /\.(css)$/, 45 | use: ['style-loader', 'css-loader'], 46 | }, 47 | { 48 | test: /\.(png|jpg|jpeg|gif|svg|woff2?|ttf|eot|otf)(\?.*$|$)/, 49 | type: 'asset/resource', 50 | generator: { 51 | filename: isProd ? 'assets/[contenthash][ext]' : 'assets/[name][ext]', 52 | }, 53 | }, 54 | { 55 | test: /\.(m?js)$/, 56 | resolve: { 57 | fullySpecified: false, 58 | }, 59 | }, 60 | ], 61 | }, 62 | devServer: { 63 | static: './dist', 64 | port: 9001, 65 | // Allow Bridge running in a container to connect to the plugin dev server. 66 | allowedHosts: 'all', 67 | headers: { 68 | 'Access-Control-Allow-Origin': '*', 69 | 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', 70 | 'Access-Control-Allow-Headers': 'X-Requested-With, Content-Type, Authorization', 71 | }, 72 | devMiddleware: { 73 | writeToDisk: true, 74 | }, 75 | }, 76 | plugins: [ 77 | new ConsoleRemotePlugin(), 78 | new CopyWebpackPlugin({ 79 | patterns: [{ from: path.resolve(__dirname, 'locales'), to: 'locales' }], 80 | }), 81 | ], 82 | devtool: isProd ? false : 'source-map', 83 | optimization: { 84 | chunkIds: isProd ? 'deterministic' : 'named', 85 | minimize: isProd, 86 | }, 87 | }; 88 | 89 | export default config; 90 | --------------------------------------------------------------------------------