├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── app-config.production.yaml ├── app-config.yaml ├── backstage.json ├── catalog-info.yaml ├── examples ├── entities.yaml ├── org.yaml └── template │ ├── content │ ├── catalog-info.yaml │ ├── index.js │ └── package.json │ └── template.yaml ├── img ├── tekton-horizontal-color.png └── tekton.png ├── lerna.json ├── package.json ├── packages ├── README.md ├── app │ ├── .eslintignore │ ├── .eslintrc.js │ ├── e2e-tests │ │ └── app.test.ts │ ├── package.json │ ├── public │ │ ├── android-chrome-192x192.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── manifest.json │ │ ├── robots.txt │ │ └── safari-pinned-tab.svg │ └── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── apis.ts │ │ ├── components │ │ ├── Root │ │ │ ├── LogoFull.tsx │ │ │ ├── LogoIcon.tsx │ │ │ ├── Root.tsx │ │ │ └── index.ts │ │ ├── catalog │ │ │ └── EntityPage.tsx │ │ └── search │ │ │ └── SearchPage.tsx │ │ ├── index.tsx │ │ └── setupTests.ts └── backend │ ├── .eslintrc.js │ ├── Dockerfile │ ├── README.md │ ├── package.json │ └── src │ ├── index.test.ts │ ├── index.ts │ ├── plugins │ ├── app.ts │ ├── auth.ts │ ├── catalog.ts │ ├── kubernetes.ts │ ├── proxy.ts │ ├── scaffolder.ts │ ├── search.ts │ └── techdocs.ts │ └── types.ts ├── playwright.config.ts ├── plugins ├── README.md └── tekton-pipelines │ ├── .eslintrc.js │ ├── README.md │ ├── dev │ └── index.tsx │ ├── package.json │ └── src │ ├── Router.tsx │ ├── __fixtures__ │ ├── log.json │ ├── pipelineRun1.json │ ├── pipelineRun2.json │ └── pipelineRun3.json │ ├── components │ ├── CollapsibleTable │ │ ├── CollapsibleTable.test.tsx │ │ ├── CollapsibleTable.tsx │ │ ├── __fixtures__ │ │ │ └── pipelinerun.json │ │ └── index.ts │ ├── CollapsibleTableRow │ │ ├── CollapsibleTableRow.tsx │ │ └── index.ts │ ├── StepLog │ │ ├── StepLog.tsx │ │ └── index.ts │ ├── StepRow │ │ ├── StepRow.tsx │ │ └── index.ts │ ├── TaskRunRow │ │ ├── TaskRunRow.tsx │ │ └── index.ts │ └── TektonDashboard │ │ ├── TektoDashboardComponent.tsx │ │ ├── __fixtures__ │ │ ├── cluster.json │ │ ├── clusters.json │ │ └── pipelinerun.json │ │ └── index.ts │ ├── index.ts │ ├── plugin.test.ts │ ├── plugin.ts │ ├── routes.ts │ ├── setupTests.ts │ └── types │ ├── index.ts │ └── types.ts ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .yarn/cache 3 | .yarn/install-state.gz 4 | node_modules 5 | packages/*/src 6 | packages/*/node_modules 7 | plugins 8 | *.local.yaml 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | playwright.config.ts 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # macOS 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Coverage directory generated when running tests with coverage 13 | coverage 14 | 15 | # Dependencies 16 | node_modules/ 17 | 18 | # Yarn 3 files 19 | .pnp.* 20 | .yarn/* 21 | !.yarn/patches 22 | !.yarn/plugins 23 | !.yarn/releases 24 | !.yarn/sdks 25 | !.yarn/versions 26 | 27 | # Node version directives 28 | .nvmrc 29 | 30 | # dotenv environment variables file 31 | .env 32 | .env.test 33 | 34 | # Build output 35 | dist 36 | dist-types 37 | 38 | # Temporary change files created by Vim 39 | *.swp 40 | 41 | # MkDocs build output 42 | site 43 | 44 | # Local configuration files 45 | *.local.yaml 46 | 47 | # Sensitive credentials 48 | *-credentials.yaml 49 | 50 | # vscode database functionality support files 51 | *.session.sql 52 | 53 | # E2E test reports 54 | e2e-test-report/ 55 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | dist-types 3 | coverage 4 | .vscode 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "node", 5 | "name": "vscode-jest-tests.v2", 6 | "request": "launch", 7 | "console": "integratedTerminal", 8 | "internalConsoleOptions": "neverOpen", 9 | "disableOptimisticBPs": true, 10 | "cwd": "${workspaceFolder}", 11 | "runtimeExecutable": "yarn", 12 | "args": [ 13 | "test", 14 | "--runInBand", 15 | "--watchAll=false", 16 | "--testNamePattern", 17 | "${jest.testNamePattern}", 18 | "--runTestsByPath", 19 | "${jest.testFile}" 20 | ] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "jest.jestCommandLine": "yarn test" 3 | } -------------------------------------------------------------------------------- /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 2020 The Backstage Authors 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tekton Pipelines Plugin Documentation 2 | 3 | 1. [Overview](#overview) 4 | 2. [Pre-requirements](#pre-requirements) 5 | 3. [Adding Tekton Frontend Plugin to Your Backstage App](#adding-tekton-frontend-plugin-to-your-backstage-app) 6 | 4. [Using the Plugin](#using-the-plugin) 7 | 5. [Developing the Tekton Pipelines Plugin Locally](#developing-the-tekton-pipelines-plugin-locally) 8 | 9 | # Overview 10 | 11 | The Tekton Pipelines plugin provides integration with Tekton Pipelines in Backstage. It allows users to view Tekton `PipelineRuns` associated with their components. 12 | 13 | - The frontend plugin is located under `plugins\tekton-pipelines`. 14 | 15 | ![Dashboard](https://github.com/jquad-group/backstage-jquad/blob/main/img/tekton.png) 16 | 17 | 18 | # Pre-requirements 19 | 20 | Before using the Tekton Pipelines plugin, make sure you have fulfilled the following pre-requisites: 21 | 22 | 1. Install and configure the Backstage backend Kubernetes plugin on your Backstage instance. Follow the installation guide at: https://backstage.io/docs/features/kubernetes/installation. 23 | 24 | 2. Configure the Kubernetes plugin by following the instructions at: https://backstage.io/docs/features/kubernetes/configuration. 25 | 26 | 3. Ensure that the necessary permissions are set in the `backstage-read-only` Cluster Role to access Tekton resources: 27 | 28 | ``` 29 | // ... 30 | # Access Tekton Resources 31 | - apiGroups: 32 | - tekton.dev 33 | resources: 34 | - pipelineruns 35 | - taskruns 36 | verbs: 37 | - get 38 | - list 39 | - watch 40 | // ... 41 | # Access Step Logs 42 | - apiGroups: 43 | - '*' 44 | resources: 45 | - pods/log # can download pod logs 46 | ``` 47 | 48 | # Adding Tekton Frontend Plugin to Your Backstage App 49 | 50 | To incorporate the Tekton Pipelines plugin into your custom Backstage app, follow these steps: 51 | 52 | 1. Navigate to `./packages/app`, and install the frontend plugin using yarn: 53 | `yarn add @jquad-group/backstage-plugin-tekton-pipelines-plugin@1.1.0` 54 | 55 | 2. In your Backstage app, navigate to the file `./packages/app/src/components/catalog/EntityPage.tsx` and add the following code: 56 | 57 | ``` 58 | import { EntityTektonPipelinesContent, isTektonCiAvailable } from '@jquad-group/backstage-plugin-tekton-pipelines-plugin'; 59 | // ... 60 | const serviceEntityPage = ( 61 | // ... 62 | 63 | 64 | 65 | Boolean(isTektonCiAvailable(e))}> 66 | 67 | 68 | 69 | 70 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | // ... 82 | ); 83 | 84 | ``` 85 | 86 | # Using the Plugin 87 | 88 | To enable Tekton Pipelines for a Component entity, add the annotation `tektonci/enabled: "true"` in addition to the existing `backstage.io/kubernetes-id`, `backstage.io/kubernetes-namespace`, or `backstage.io/kubernetes-label-selector` annotations. For example: 89 | 90 | ``` 91 | apiVersion: backstage.io/v1alpha1 92 | kind: Component 93 | metadata: 94 | namespace: dev 95 | annotations: 96 | backstage.io/kubernetes-label-selector: 'app=microservice' 97 | tektonci/enabled: "true" 98 | name: microservice 99 | description: Microservice 100 | spec: 101 | type: service 102 | lifecycle: production 103 | owner: user:guest 104 | ``` 105 | 106 | This will list all the `PipelineRuns` having the label `app: microservice`, e.g. 107 | 108 | ``` 109 | apiVersion: tekton.dev/v1 110 | kind: PipelineRun 111 | metadata: 112 | generateName: microservice-pipeline- 113 | namespace: dev 114 | labels: 115 | app: microservice 116 | ``` 117 | 118 | You can also configure the tekton dashboard url for each cluster by adding the annotation `tektonci.[clusterName]/dashboard` to the catalog info. 119 | For example, having a kubernetes configuration for the clusters `rancher` and `k3d`: 120 | 121 | ``` 122 | kubernetes: 123 | ... 124 | - type: 'config' 125 | clusters: 126 | - url: http://host.docker.internal:21301 127 | name: k3d 128 | ... 129 | - url: https://rancher.example.com:6443 130 | name: rancher 131 | ... 132 | ``` 133 | 134 | one can configure a separate url for `rancher` and `k3d` for the tekton dashboard like this: 135 | 136 | ``` 137 | apiVersion: backstage.io/v1alpha1 138 | kind: Component 139 | metadata: 140 | namespace: dev 141 | annotations: 142 | backstage.io/kubernetes-label-selector: 'app=microservice' 143 | tektonci/enabled: "true" 144 | tektonci.k3d/dashboard: http://localhost:8080/tekton/$namespace/$pipelinerun 145 | tektonci.rancher/dashboard: https://rancher.example.com:8080/tekton/$namespace/$pipelinerun 146 | name: microservice 147 | description: Microservice 148 | spec: 149 | type: service 150 | lifecycle: production 151 | owner: user:guest 152 | ``` 153 | 154 | In the above example `$namespace` and `$pipelinerun` are variables, that are automatically interpolated on runtime from the plugin. 155 | 156 | ## Using an older Tekton API Version 157 | 158 | Per default, the plugin looks for `v1` API Version. If you want to override this behavior, and use `v1beta1` instead, add the following annotation in a `Component`: 159 | 160 | ``` 161 | annotations: 162 | tektonci/api: v1beta1 163 | ``` 164 | 165 | # Developing the Tekton Pipelines Plugin Locally 166 | 167 | To work on the Tekton Pipelines plugin locally, follow these steps: 168 | 1. Navigate to the main directory of the Backstage app 169 | 2. Start the development server: `yarn dev` 170 | 3. Access the local development environment by opening your web browser and visiting: `http://localhost:3000/`. 171 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /app-config.production.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | # Should be the same as backend.baseUrl when using the `app-backend` plugin. 3 | baseUrl: http://localhost:7007 4 | 5 | backend: 6 | # Note that the baseUrl should be the URL that the browser and other clients 7 | # should use when communicating with the backend, i.e. it needs to be 8 | # reachable not just from within the backend host, but from all of your 9 | # callers. When its value is "http://localhost:7007", it's strictly private 10 | # and can't be reached by others. 11 | baseUrl: http://localhost:7007 12 | listen: 13 | port: 7007 14 | # The following host directive binds to all IPv4 interfaces when its value 15 | # is "0.0.0.0". This is the most permissive setting. The right value depends 16 | # on your specific deployment. If you remove the host line entirely, the 17 | # backend will bind on the interface that corresponds to the backend.baseUrl 18 | # hostname. 19 | host: 0.0.0.0 20 | 21 | # config options: https://node-postgres.com/api/client 22 | database: 23 | client: pg 24 | connection: 25 | host: ${POSTGRES_HOST} 26 | port: ${POSTGRES_PORT} 27 | user: ${POSTGRES_USER} 28 | password: ${POSTGRES_PASSWORD} 29 | # https://node-postgres.com/features/ssl 30 | # you can set the sslmode configuration option via the `PGSSLMODE` environment variable 31 | # see https://www.postgresql.org/docs/current/libpq-ssl.html Table 33.1. SSL Mode Descriptions (e.g. require) 32 | # ssl: 33 | # ca: # if you have a CA file and want to verify it you can uncomment this section 34 | # $file: /ca/server.crt 35 | 36 | catalog: 37 | # Overrides the default list locations from app-config.yaml as these contain example data. 38 | # See https://backstage.io/docs/features/software-catalog/software-catalog-overview#adding-components-to-the-catalog for more details 39 | # on how to get entities into the catalog. 40 | locations: [] 41 | 42 | tekton: 43 | baseUrl: https://example.domain.com 44 | authorizationBearerToken: 'xxx' 45 | dashboardBaseUrl: https://example.domain.com 46 | 47 | tekton: 48 | - baseUrl: https://example1.domain.com 49 | authorizationBearerToken: 'xxx' 50 | dashboardBaseUrl: https://example1.domain.com 51 | # multiple kubernetes clusters 52 | # - baseUrl: https://example2.domain.com 53 | # authorizationBearerToken: 'yyy' 54 | # dashboardBaseUrl: https://example2.domain.com 55 | -------------------------------------------------------------------------------- /app-config.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | title: Scaffolded Backstage App 3 | baseUrl: http://localhost:3000 4 | 5 | organization: 6 | name: My Company 7 | 8 | backend: 9 | # Used for enabling authentication, secret is shared by all backend plugins 10 | # See https://backstage.io/docs/tutorials/backend-to-backend-auth for 11 | # information on the format 12 | # auth: 13 | # keys: 14 | # - secret: ${BACKEND_SECRET} 15 | baseUrl: http://localhost:7007 16 | listen: 17 | port: 7007 18 | # Uncomment the following host directive to bind to all IPv4 interfaces and 19 | # not just the baseUrl hostname. 20 | # host: 0.0.0.0 21 | csp: 22 | connect-src: ["'self'", 'http:', 'https:'] 23 | # Content-Security-Policy directives follow the Helmet format: https://helmetjs.github.io/#reference 24 | # Default Helmet Content-Security-Policy values can be removed by setting the key to false 25 | cors: 26 | origin: http://localhost:3000 27 | methods: [GET, POST, PUT, DELETE] 28 | credentials: true 29 | # This is for local developement only, it is not recommended to use this in production 30 | # The production database configuration is stored in app-config.production.yaml 31 | database: 32 | client: better-sqlite3 33 | connection: ':memory:' 34 | cache: 35 | store: memory 36 | # workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir 37 | 38 | integrations: 39 | github: 40 | - host: github.com 41 | # This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information 42 | # about setting up the GitHub integration here: https://backstage.io/docs/getting-started/configuration#setting-up-a-github-integration 43 | token: ${GITHUB_TOKEN} 44 | ### Example for how to add your GitHub Enterprise instance using the API: 45 | # - host: ghe.example.net 46 | # apiBaseUrl: https://ghe.example.net/api/v3 47 | # token: ${GHE_TOKEN} 48 | 49 | proxy: 50 | '/test': 51 | target: 'https://example.com' 52 | changeOrigin: true 53 | 54 | # Reference documentation http://backstage.io/docs/features/techdocs/configuration 55 | # Note: After experimenting with basic setup, use CI/CD to generate docs 56 | # and an external cloud storage when deploying TechDocs for production use-case. 57 | # https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach 58 | techdocs: 59 | builder: 'local' # Alternatives - 'external' 60 | generator: 61 | runIn: 'docker' # Alternatives - 'local' 62 | publisher: 63 | type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives. 64 | 65 | auth: 66 | # see https://backstage.io/docs/auth/ to learn about auth providers 67 | providers: {} 68 | 69 | scaffolder: 70 | # see https://backstage.io/docs/features/software-templates/configuration for software template options 71 | 72 | catalog: 73 | import: 74 | entityFilename: catalog-info.yaml 75 | pullRequestBranchName: backstage-integration 76 | rules: 77 | - allow: [Component, System, API, Resource, Location] 78 | locations: 79 | # Local example data, file locations are relative to the backend process, typically `packages/backend` 80 | - type: file 81 | target: ../../examples/entities.yaml 82 | 83 | # Local example template 84 | - type: file 85 | target: ../../examples/template/template.yaml 86 | rules: 87 | - allow: [Template] 88 | 89 | # Local example organizational data 90 | - type: file 91 | target: ../../examples/org.yaml 92 | rules: 93 | - allow: [User, Group] 94 | 95 | ## Uncomment these lines to add more example data 96 | - type: url 97 | target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml 98 | 99 | ## Uncomment these lines to add an example org 100 | - type: url 101 | target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml 102 | rules: 103 | - allow: [User, Group] 104 | 105 | tekton: 106 | - name: cluster1 107 | baseUrl: https://example1.domain.com 108 | authorizationBearerToken: 'xxx' 109 | dashboardBaseUrl: https://example1.domain.com 110 | externalLogs: 111 | - enabled: true 112 | urlTemplate: https://externalBaseUrl/$namespace/$taskRunPodName/$stepContainer.txt 113 | headers: [ 114 | "Content-Type", 115 | "plain/text", 116 | "Authorization", 117 | "AWS AKIAIOSFODNN7EXAMPLE:qgk2+6Sv9/oM7G3qLEjTH1a1l1g=" 118 | ] 119 | # multiple kubernetes clusters 120 | # - name: cluster2 121 | # baseUrl: https://example2.domain.com 122 | # authorizationBearerToken: 'yyy' 123 | # dashboardBaseUrl: https://example2.domain.com -------------------------------------------------------------------------------- /backstage.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.22.1" 3 | } 4 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: jquad-backstage 5 | description: An example of a Backstage application. 6 | # Example for optional annotations 7 | # annotations: 8 | # github.com/project-slug: backstage/backstage 9 | # backstage.io/techdocs-ref: dir:. 10 | spec: 11 | type: website 12 | owner: john@example.com 13 | lifecycle: experimental 14 | -------------------------------------------------------------------------------- /examples/entities.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-system 3 | apiVersion: backstage.io/v1alpha1 4 | kind: System 5 | metadata: 6 | name: examples 7 | spec: 8 | owner: guests 9 | --- 10 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component 11 | apiVersion: backstage.io/v1alpha1 12 | kind: Component 13 | metadata: 14 | name: example-website 15 | spec: 16 | type: website 17 | lifecycle: experimental 18 | owner: guests 19 | system: examples 20 | providesApis: [example-grpc-api] 21 | --- 22 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-api 23 | apiVersion: backstage.io/v1alpha1 24 | kind: API 25 | metadata: 26 | name: example-grpc-api 27 | spec: 28 | type: grpc 29 | lifecycle: experimental 30 | owner: guests 31 | system: examples 32 | definition: | 33 | syntax = "proto3"; 34 | 35 | service Exampler { 36 | rpc Example (ExampleMessage) returns (ExampleMessage) {}; 37 | } 38 | 39 | message ExampleMessage { 40 | string example = 1; 41 | }; 42 | -------------------------------------------------------------------------------- /examples/org.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-user 3 | apiVersion: backstage.io/v1alpha1 4 | kind: User 5 | metadata: 6 | name: guest 7 | spec: 8 | memberOf: [guests] 9 | --- 10 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-group 11 | apiVersion: backstage.io/v1alpha1 12 | kind: Group 13 | metadata: 14 | name: guests 15 | spec: 16 | type: team 17 | children: [] 18 | -------------------------------------------------------------------------------- /examples/template/content/catalog-info.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: backstage.io/v1alpha1 2 | kind: Component 3 | metadata: 4 | name: ${{ values.name | dump }} 5 | spec: 6 | type: service 7 | owner: user:guest 8 | lifecycle: experimental 9 | -------------------------------------------------------------------------------- /examples/template/content/index.js: -------------------------------------------------------------------------------- 1 | console.log('Hello from ${{ values.name }}!'); 2 | -------------------------------------------------------------------------------- /examples/template/content/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "${{ values.name }}", 3 | "private": true, 4 | "dependencies": {} 5 | } 6 | -------------------------------------------------------------------------------- /examples/template/template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scaffolder.backstage.io/v1beta3 2 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-template 3 | kind: Template 4 | metadata: 5 | name: example-nodejs-template 6 | title: Example Node.js Template 7 | description: An example template for the scaffolder that creates a simple Node.js service 8 | spec: 9 | owner: user:guest 10 | type: service 11 | 12 | # These parameters are used to generate the input form in the frontend, and are 13 | # used to gather input data for the execution of the template. 14 | parameters: 15 | - title: Fill in some steps 16 | required: 17 | - name 18 | properties: 19 | name: 20 | title: Name 21 | type: string 22 | description: Unique name of the component 23 | ui:autofocus: true 24 | ui:options: 25 | rows: 5 26 | - title: Choose a location 27 | required: 28 | - repoUrl 29 | properties: 30 | repoUrl: 31 | title: Repository Location 32 | type: string 33 | ui:field: RepoUrlPicker 34 | ui:options: 35 | allowedHosts: 36 | - github.com 37 | 38 | # These steps are executed in the scaffolder backend, using data that we gathered 39 | # via the parameters above. 40 | steps: 41 | # Each step executes an action, in this case one templates files into the working directory. 42 | - id: fetch-base 43 | name: Fetch Base 44 | action: fetch:template 45 | input: 46 | url: ./content 47 | values: 48 | name: ${{ parameters.name }} 49 | 50 | # This step publishes the contents of the working directory to GitHub. 51 | - id: publish 52 | name: Publish 53 | action: publish:github 54 | input: 55 | allowedHosts: ['github.com'] 56 | description: This is ${{ parameters.name }} 57 | repoUrl: ${{ parameters.repoUrl }} 58 | 59 | # The final step is to register our new component in the catalog. 60 | - id: register 61 | name: Register 62 | action: catalog:register 63 | input: 64 | repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }} 65 | catalogInfoPath: '/catalog-info.yaml' 66 | 67 | # Outputs are displayed to the user after a successful execution of the template. 68 | output: 69 | links: 70 | - title: Repository 71 | url: ${{ steps['publish'].output.remoteUrl }} 72 | - title: Open in catalog 73 | icon: catalog 74 | entityRef: ${{ steps['register'].output.entityRef }} 75 | -------------------------------------------------------------------------------- /img/tekton-horizontal-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jquad-group/backstage-jquad/8d6de94cdde46bc2fc85164d087b36b3be8d8521/img/tekton-horizontal-color.png -------------------------------------------------------------------------------- /img/tekton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jquad-group/backstage-jquad/8d6de94cdde46bc2fc85164d087b36b3be8d8521/img/tekton.png -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": ["packages/*", "plugins/*"], 3 | "npmClient": "yarn", 4 | "version": "0.1.0", 5 | "$schema": "node_modules/lerna/schemas/lerna-schema.json" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "1.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": "18 || 20" 7 | }, 8 | "scripts": { 9 | "dev": "concurrently \"yarn start\" \"yarn start-backend\"", 10 | "start": "yarn workspace app start", 11 | "start-backend": "yarn workspace backend start", 12 | "build:backend": "yarn workspace backend build", 13 | "build:all": "backstage-cli repo build --all", 14 | "build-image": "yarn workspace backend build-image", 15 | "tsc": "tsc", 16 | "tsc:full": "tsc --skipLibCheck false --incremental false", 17 | "clean": "backstage-cli repo clean", 18 | "test": "backstage-cli repo test", 19 | "test:all": "backstage-cli repo test --coverage", 20 | "test:e2e": "playwright test", 21 | "fix": "backstage-cli repo fix", 22 | "lint": "backstage-cli repo lint --since origin/master", 23 | "lint:all": "backstage-cli repo lint", 24 | "prettier:check": "prettier --check .", 25 | "new": "backstage-cli new --scope internal" 26 | }, 27 | "workspaces": { 28 | "packages": [ 29 | "packages/*", 30 | "plugins/*" 31 | ] 32 | }, 33 | "devDependencies": { 34 | "@backstage/cli": "^0.25.1", 35 | "@backstage/e2e-test-utils": "^0.1.0", 36 | "@playwright/test": "^1.32.3", 37 | "@spotify/prettier-config": "^12.0.0", 38 | "concurrently": "^8.2.2", 39 | "lerna": "^7.3.0", 40 | "node-gyp": "^9.0.0", 41 | "prettier": "^2.3.2", 42 | "typescript": "~5.2.0" 43 | }, 44 | "resolutions": { 45 | "@types/react": "^18", 46 | "@types/react-dom": "^18" 47 | }, 48 | "prettier": "@spotify/prettier-config", 49 | "lint-staged": { 50 | "*.{js,jsx,ts,tsx,mjs,cjs}": [ 51 | "eslint --fix", 52 | "prettier --write" 53 | ], 54 | "*.{json,md}": [ 55 | "prettier --write" 56 | ] 57 | }, 58 | "dependencies": { 59 | "@material-ui/core": "4.11.2", 60 | "react": "^18.2.0", 61 | "react-dom": "^18.2.0" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/README.md: -------------------------------------------------------------------------------- 1 | # The Packages Folder 2 | 3 | This is where your own applications and centrally managed libraries live, each 4 | in a separate folder of its own. 5 | 6 | From the start there's an `app` folder (for the frontend) and a `backend` folder 7 | (for the Node backend), but you can also add more modules in here that house 8 | your core additions and adaptations, such as themes, common React component 9 | libraries, utilities, and similar. 10 | -------------------------------------------------------------------------------- /packages/app/.eslintignore: -------------------------------------------------------------------------------- 1 | public 2 | -------------------------------------------------------------------------------- /packages/app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /packages/app/e2e-tests/app.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Backstage 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 | 17 | import { test, expect } from '@playwright/test'; 18 | 19 | test('App should render the welcome page', async ({ page }) => { 20 | await page.goto('/'); 21 | 22 | await expect(page.getByText('My Company Catalog')).toBeVisible(); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "bundled": true, 6 | "backstage": { 7 | "role": "frontend" 8 | }, 9 | "scripts": { 10 | "start": "backstage-cli package start", 11 | "build": "backstage-cli package build", 12 | "clean": "backstage-cli package clean", 13 | "test": "backstage-cli package test", 14 | "lint": "backstage-cli package lint" 15 | }, 16 | "dependencies": { 17 | "@backstage/app-defaults": "^1.4.7", 18 | "@backstage/catalog-model": "^1.4.3", 19 | "@backstage/cli": "^0.25.1", 20 | "@backstage/core-app-api": "^1.11.3", 21 | "@backstage/core-components": "^0.13.10", 22 | "@backstage/core-plugin-api": "^1.8.2", 23 | "@backstage/integration-react": "^1.1.23", 24 | "@backstage/plugin-api-docs": "^0.10.3", 25 | "@backstage/plugin-catalog": "^1.16.1", 26 | "@backstage/plugin-catalog-common": "^1.0.20", 27 | "@backstage/plugin-catalog-graph": "^0.3.3", 28 | "@backstage/plugin-catalog-import": "^0.10.5", 29 | "@backstage/plugin-catalog-react": "^1.9.3", 30 | "@backstage/plugin-github-actions": "^0.6.10", 31 | "@backstage/plugin-kubernetes": "^0.11.4", 32 | "@backstage/plugin-org": "^0.6.19", 33 | "@backstage/plugin-permission-react": "^0.4.19", 34 | "@backstage/plugin-scaffolder": "^1.17.1", 35 | "@backstage/plugin-search": "^1.4.5", 36 | "@backstage/plugin-search-react": "^1.7.5", 37 | "@backstage/plugin-tech-radar": "^0.6.12", 38 | "@backstage/plugin-techdocs": "^1.9.3", 39 | "@backstage/plugin-techdocs-module-addons-contrib": "^1.1.4", 40 | "@backstage/plugin-techdocs-react": "^1.1.15", 41 | "@backstage/plugin-user-settings": "^0.8.0", 42 | "@backstage/theme": "^0.5.0", 43 | "@material-ui/core": "^4.12.2", 44 | "@material-ui/icons": "^4.9.1", 45 | "history": "^5.0.0", 46 | "react": "^18.0.2", 47 | "react-dom": "^18.0.2", 48 | "react-router": "^6.3.0", 49 | "react-router-dom": "^6.3.0", 50 | "react-use": "^17.2.4" 51 | }, 52 | "devDependencies": { 53 | "@backstage/test-utils": "^1.4.7", 54 | "@playwright/test": "^1.32.3", 55 | "@testing-library/dom": "^9.0.0", 56 | "@testing-library/jest-dom": "^6.0.0", 57 | "@testing-library/react": "^14.0.0", 58 | "@testing-library/user-event": "^14.0.0", 59 | "@types/react-dom": "*", 60 | "cross-env": "^7.0.0" 61 | }, 62 | "browserslist": { 63 | "production": [ 64 | ">0.2%", 65 | "not dead", 66 | "not op_mini all" 67 | ], 68 | "development": [ 69 | "last 1 chrome version", 70 | "last 1 firefox version", 71 | "last 1 safari version" 72 | ] 73 | }, 74 | "files": [ 75 | "dist" 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /packages/app/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jquad-group/backstage-jquad/8d6de94cdde46bc2fc85164d087b36b3be8d8521/packages/app/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /packages/app/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jquad-group/backstage-jquad/8d6de94cdde46bc2fc85164d087b36b3be8d8521/packages/app/public/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/app/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jquad-group/backstage-jquad/8d6de94cdde46bc2fc85164d087b36b3be8d8521/packages/app/public/favicon-16x16.png -------------------------------------------------------------------------------- /packages/app/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jquad-group/backstage-jquad/8d6de94cdde46bc2fc85164d087b36b3be8d8521/packages/app/public/favicon-32x32.png -------------------------------------------------------------------------------- /packages/app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jquad-group/backstage-jquad/8d6de94cdde46bc2fc85164d087b36b3be8d8521/packages/app/public/favicon.ico -------------------------------------------------------------------------------- /packages/app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 15 | 20 | 21 | 22 | 27 | 33 | 39 | 44 | <%= config.getString('app.title') %> 45 | 46 | 47 | 48 |
49 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /packages/app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Backstage", 3 | "name": "Backstage", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "48x48", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/app/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /packages/app/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | Created by potrace 1.11, written by Peter Selinger 2001-2013 -------------------------------------------------------------------------------- /packages/app/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, waitFor } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | describe('App', () => { 6 | it('should render', async () => { 7 | process.env = { 8 | NODE_ENV: 'test', 9 | APP_CONFIG: [ 10 | { 11 | data: { 12 | app: { title: 'Test' }, 13 | backend: { baseUrl: 'http://localhost:7007' }, 14 | techdocs: { 15 | storageUrl: 'http://localhost:7007/api/techdocs/static/docs', 16 | }, 17 | }, 18 | context: 'test', 19 | }, 20 | ] as any, 21 | }; 22 | 23 | const rendered = render(); 24 | 25 | await waitFor(() => { 26 | expect(rendered.baseElement).toBeInTheDocument(); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Navigate, Route } from 'react-router-dom'; 3 | import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; 4 | import { 5 | CatalogEntityPage, 6 | CatalogIndexPage, 7 | catalogPlugin, 8 | } from '@backstage/plugin-catalog'; 9 | import { 10 | CatalogImportPage, 11 | catalogImportPlugin, 12 | } from '@backstage/plugin-catalog-import'; 13 | import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder'; 14 | import { orgPlugin } from '@backstage/plugin-org'; 15 | import { SearchPage } from '@backstage/plugin-search'; 16 | import { TechRadarPage } from '@backstage/plugin-tech-radar'; 17 | import { 18 | TechDocsIndexPage, 19 | techdocsPlugin, 20 | TechDocsReaderPage, 21 | } from '@backstage/plugin-techdocs'; 22 | import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; 23 | import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; 24 | import { UserSettingsPage } from '@backstage/plugin-user-settings'; 25 | import { apis } from './apis'; 26 | import { entityPage } from './components/catalog/EntityPage'; 27 | import { searchPage } from './components/search/SearchPage'; 28 | import { Root } from './components/Root'; 29 | 30 | import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components'; 31 | import { createApp } from '@backstage/app-defaults'; 32 | import { AppRouter, FlatRoutes } from '@backstage/core-app-api'; 33 | import { CatalogGraphPage } from '@backstage/plugin-catalog-graph'; 34 | import { RequirePermission } from '@backstage/plugin-permission-react'; 35 | import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha'; 36 | /* ignore lint error for internal dependencies */ 37 | /* eslint-disable */ 38 | import { EntityTektonPipelinesContent } from '@jquad-group/backstage-plugin-tekton-pipelines-plugin'; 39 | 40 | const app = createApp({ 41 | apis, 42 | bindRoutes({ bind }) { 43 | bind(catalogPlugin.externalRoutes, { 44 | createComponent: scaffolderPlugin.routes.root, 45 | viewTechDoc: techdocsPlugin.routes.docRoot, 46 | createFromTemplate: scaffolderPlugin.routes.selectedTemplate, 47 | }); 48 | bind(apiDocsPlugin.externalRoutes, { 49 | registerApi: catalogImportPlugin.routes.importPage, 50 | }); 51 | bind(scaffolderPlugin.externalRoutes, { 52 | registerComponent: catalogImportPlugin.routes.importPage, 53 | viewTechDoc: techdocsPlugin.routes.docRoot, 54 | }); 55 | bind(orgPlugin.externalRoutes, { 56 | catalogIndex: catalogPlugin.routes.catalogIndex, 57 | }); 58 | }, 59 | }); 60 | 61 | const routes = ( 62 | 63 | } /> 64 | } /> 65 | } 68 | > 69 | {entityPage} 70 | 71 | } /> 72 | } 75 | > 76 | 77 | 78 | 79 | 80 | } /> 81 | } /> 82 | } 85 | /> 86 | 90 | 91 | 92 | } 93 | /> 94 | }> 95 | {searchPage} 96 | 97 | } /> 98 | } /> 99 | } /> 100 | 101 | ); 102 | 103 | export default app.createRoot( 104 | <> 105 | 106 | 107 | 108 | {routes} 109 | 110 | , 111 | ); 112 | -------------------------------------------------------------------------------- /packages/app/src/apis.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ScmIntegrationsApi, 3 | scmIntegrationsApiRef, 4 | ScmAuth, 5 | } from '@backstage/integration-react'; 6 | import { 7 | AnyApiFactory, 8 | configApiRef, 9 | createApiFactory, 10 | } from '@backstage/core-plugin-api'; 11 | 12 | export const apis: AnyApiFactory[] = [ 13 | createApiFactory({ 14 | api: scmIntegrationsApiRef, 15 | deps: { configApi: configApiRef }, 16 | factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), 17 | }), 18 | ScmAuth.createDefaultApiFactory(), 19 | ]; 20 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/LogoFull.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core'; 3 | 4 | const useStyles = makeStyles({ 5 | svg: { 6 | width: 'auto', 7 | height: 30, 8 | }, 9 | path: { 10 | fill: '#7df3e1', 11 | }, 12 | }); 13 | const LogoFull = () => { 14 | const classes = useStyles(); 15 | 16 | return ( 17 | 22 | 26 | 27 | ); 28 | }; 29 | 30 | export default LogoFull; 31 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/LogoIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles } from '@material-ui/core'; 3 | 4 | const useStyles = makeStyles({ 5 | svg: { 6 | width: 'auto', 7 | height: 28, 8 | }, 9 | path: { 10 | fill: '#7df3e1', 11 | }, 12 | }); 13 | 14 | const LogoIcon = () => { 15 | const classes = useStyles(); 16 | 17 | return ( 18 | 23 | 27 | 28 | ); 29 | }; 30 | 31 | export default LogoIcon; 32 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/Root.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react'; 2 | import { makeStyles } from '@material-ui/core'; 3 | import HomeIcon from '@material-ui/icons/Home'; 4 | import ExtensionIcon from '@material-ui/icons/Extension'; 5 | import MapIcon from '@material-ui/icons/MyLocation'; 6 | import LibraryBooks from '@material-ui/icons/LibraryBooks'; 7 | import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; 8 | import LogoFull from './LogoFull'; 9 | import LogoIcon from './LogoIcon'; 10 | import { 11 | Settings as SidebarSettings, 12 | UserSettingsSignInAvatar, 13 | } from '@backstage/plugin-user-settings'; 14 | import { SidebarSearchModal } from '@backstage/plugin-search'; 15 | import { 16 | Sidebar, 17 | sidebarConfig, 18 | SidebarDivider, 19 | SidebarGroup, 20 | SidebarItem, 21 | SidebarPage, 22 | SidebarScrollWrapper, 23 | SidebarSpace, 24 | useSidebarOpenState, 25 | Link, 26 | } from '@backstage/core-components'; 27 | import MenuIcon from '@material-ui/icons/Menu'; 28 | import SearchIcon from '@material-ui/icons/Search'; 29 | 30 | const useSidebarLogoStyles = makeStyles({ 31 | root: { 32 | width: sidebarConfig.drawerWidthClosed, 33 | height: 3 * sidebarConfig.logoHeight, 34 | display: 'flex', 35 | flexFlow: 'row nowrap', 36 | alignItems: 'center', 37 | marginBottom: -14, 38 | }, 39 | link: { 40 | width: sidebarConfig.drawerWidthClosed, 41 | marginLeft: 24, 42 | }, 43 | }); 44 | 45 | const SidebarLogo = () => { 46 | const classes = useSidebarLogoStyles(); 47 | const { isOpen } = useSidebarOpenState(); 48 | 49 | return ( 50 |
51 | 52 | {isOpen ? : } 53 | 54 |
55 | ); 56 | }; 57 | 58 | export const Root = ({ children }: PropsWithChildren<{}>) => ( 59 | 60 | 61 | 62 | } to="/search"> 63 | 64 | 65 | 66 | }> 67 | {/* Global nav, not org-specific */} 68 | 69 | 70 | 71 | 72 | {/* End global nav */} 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | } 83 | to="/settings" 84 | > 85 | 86 | 87 | 88 | {children} 89 | 90 | ); 91 | -------------------------------------------------------------------------------- /packages/app/src/components/Root/index.ts: -------------------------------------------------------------------------------- 1 | export { Root } from './Root'; 2 | -------------------------------------------------------------------------------- /packages/app/src/components/catalog/EntityPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, Grid } from '@material-ui/core'; 3 | import { 4 | EntityApiDefinitionCard, 5 | EntityConsumedApisCard, 6 | EntityConsumingComponentsCard, 7 | EntityHasApisCard, 8 | EntityProvidedApisCard, 9 | EntityProvidingComponentsCard, 10 | } from '@backstage/plugin-api-docs'; 11 | import { 12 | EntityAboutCard, 13 | EntityDependsOnComponentsCard, 14 | EntityDependsOnResourcesCard, 15 | EntityHasComponentsCard, 16 | EntityHasResourcesCard, 17 | EntityHasSubcomponentsCard, 18 | EntityHasSystemsCard, 19 | EntityLayout, 20 | EntityLinksCard, 21 | EntitySwitch, 22 | EntityOrphanWarning, 23 | EntityProcessingErrorsPanel, 24 | isComponentType, 25 | isKind, 26 | hasCatalogProcessingErrors, 27 | isOrphan, 28 | hasRelationWarnings, 29 | EntityRelationWarning, 30 | } from '@backstage/plugin-catalog'; 31 | import { 32 | isGithubActionsAvailable, 33 | EntityGithubActionsContent, 34 | } from '@backstage/plugin-github-actions'; 35 | import { 36 | EntityUserProfileCard, 37 | EntityGroupProfileCard, 38 | EntityMembersListCard, 39 | EntityOwnershipCard, 40 | } from '@backstage/plugin-org'; 41 | import { EntityTechdocsContent } from '@backstage/plugin-techdocs'; 42 | import { EmptyState } from '@backstage/core-components'; 43 | import { 44 | Direction, 45 | EntityCatalogGraphCard, 46 | } from '@backstage/plugin-catalog-graph'; 47 | import { 48 | RELATION_API_CONSUMED_BY, 49 | RELATION_API_PROVIDED_BY, 50 | RELATION_CONSUMES_API, 51 | RELATION_DEPENDENCY_OF, 52 | RELATION_DEPENDS_ON, 53 | RELATION_HAS_PART, 54 | RELATION_PART_OF, 55 | RELATION_PROVIDES_API, 56 | } from '@backstage/catalog-model'; 57 | 58 | import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; 59 | import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib'; 60 | /* ignore lint error for internal dependencies */ 61 | /* eslint-disable */ 62 | import { EntityKubernetesContent } from '@backstage/plugin-kubernetes'; 63 | import { EntityTektonPipelinesContent, isTektonCiAvailable } from '@jquad-group/backstage-plugin-tekton-pipelines-plugin'; 64 | 65 | const techdocsContent = ( 66 | 67 | 68 | 69 | 70 | 71 | ); 72 | 73 | const cicdContent = ( 74 | // This is an example of how you can implement your company's logic in entity page. 75 | // You can for example enforce that all components of type 'service' should use GitHubActions 76 | 77 | 78 | 79 | 80 | 81 | 82 | 92 | Read more 93 | 94 | } 95 | /> 96 | 97 | 98 | ); 99 | 100 | const entityWarningContent = ( 101 | <> 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | ); 127 | 128 | const overviewContent = ( 129 | 130 | {entityWarningContent} 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | ); 146 | 147 | const serviceEntityPage = ( 148 | 149 | 150 | {overviewContent} 151 | 152 | 153 | 154 | {cicdContent} 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | {techdocsContent} 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | Boolean(isTektonCiAvailable(e))}> 190 | 191 | 192 | 193 | 194 | 199 | 200 | 201 | 202 | 203 | 204 | ); 205 | 206 | const websiteEntityPage = ( 207 | 208 | 209 | {overviewContent} 210 | 211 | 212 | 213 | {cicdContent} 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | {techdocsContent} 229 | 230 | 231 | ); 232 | 233 | /** 234 | * NOTE: This page is designed to work on small screens such as mobile devices. 235 | * This is based on Material UI Grid. If breakpoints are used, each grid item must set the `xs` prop to a column size or to `true`, 236 | * since this does not default. If no breakpoints are used, the items will equitably share the available space. 237 | * https://material-ui.com/components/grid/#basic-grid. 238 | */ 239 | 240 | const defaultEntityPage = ( 241 | 242 | 243 | {overviewContent} 244 | 245 | 246 | 247 | {techdocsContent} 248 | 249 | 250 | ); 251 | 252 | const componentPage = ( 253 | 254 | 255 | {serviceEntityPage} 256 | 257 | 258 | 259 | {websiteEntityPage} 260 | 261 | 262 | {defaultEntityPage} 263 | 264 | ); 265 | 266 | const apiPage = ( 267 | 268 | 269 | 270 | {entityWarningContent} 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | ); 300 | 301 | const userPage = ( 302 | 303 | 304 | 305 | {entityWarningContent} 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | ); 316 | 317 | const groupPage = ( 318 | 319 | 320 | 321 | {entityWarningContent} 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | ); 338 | 339 | const systemPage = ( 340 | 341 | 342 | 343 | {entityWarningContent} 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 382 | 383 | 384 | ); 385 | 386 | const domainPage = ( 387 | 388 | 389 | 390 | {entityWarningContent} 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | ); 404 | 405 | export const entityPage = ( 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | {defaultEntityPage} 415 | 416 | ); 417 | -------------------------------------------------------------------------------- /packages/app/src/components/search/SearchPage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { makeStyles, Theme, Grid, Paper } from '@material-ui/core'; 3 | 4 | import { CatalogSearchResultListItem } from '@backstage/plugin-catalog'; 5 | import { 6 | catalogApiRef, 7 | CATALOG_FILTER_EXISTS, 8 | } from '@backstage/plugin-catalog-react'; 9 | import { TechDocsSearchResultListItem } from '@backstage/plugin-techdocs'; 10 | 11 | import { SearchType } from '@backstage/plugin-search'; 12 | import { 13 | SearchBar, 14 | SearchFilter, 15 | SearchResult, 16 | SearchPagination, 17 | useSearch, 18 | } from '@backstage/plugin-search-react'; 19 | import { 20 | CatalogIcon, 21 | Content, 22 | DocsIcon, 23 | Header, 24 | Page, 25 | } from '@backstage/core-components'; 26 | import { useApi } from '@backstage/core-plugin-api'; 27 | 28 | const useStyles = makeStyles((theme: Theme) => ({ 29 | bar: { 30 | padding: theme.spacing(1, 0), 31 | }, 32 | filters: { 33 | padding: theme.spacing(2), 34 | marginTop: theme.spacing(2), 35 | }, 36 | filter: { 37 | '& + &': { 38 | marginTop: theme.spacing(2.5), 39 | }, 40 | }, 41 | })); 42 | 43 | const SearchPage = () => { 44 | const classes = useStyles(); 45 | const { types } = useSearch(); 46 | const catalogApi = useApi(catalogApiRef); 47 | 48 | return ( 49 | 50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | , 67 | }, 68 | { 69 | value: 'techdocs', 70 | name: 'Documentation', 71 | icon: , 72 | }, 73 | ]} 74 | /> 75 | 76 | {types.includes('techdocs') && ( 77 | { 82 | // Return a list of entities which are documented. 83 | const { items } = await catalogApi.getEntities({ 84 | fields: ['metadata.name'], 85 | filter: { 86 | 'metadata.annotations.backstage.io/techdocs-ref': 87 | CATALOG_FILTER_EXISTS, 88 | }, 89 | }); 90 | 91 | const names = items.map(entity => entity.metadata.name); 92 | names.sort(); 93 | return names; 94 | }} 95 | /> 96 | )} 97 | 103 | 109 | 110 | 111 | 112 | 113 | 114 | } /> 115 | } /> 116 | 117 | 118 | 119 | 120 | 121 | ); 122 | }; 123 | 124 | export const searchPage = ; 125 | -------------------------------------------------------------------------------- /packages/app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import '@backstage/cli/asset-types'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | import App from './App'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render(); 7 | -------------------------------------------------------------------------------- /packages/app/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /packages/backend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); 2 | -------------------------------------------------------------------------------- /packages/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # This dockerfile builds an image for the backend package. 2 | # It should be executed with the root of the repo as docker context. 3 | # 4 | # Before building this image, be sure to have run the following commands in the repo root: 5 | # 6 | # yarn install 7 | # yarn tsc 8 | # yarn build:backend 9 | # 10 | # Once the commands have been run, you can build the image using `yarn build-image` 11 | 12 | FROM node:18-bookworm-slim 13 | 14 | # Install isolate-vm dependencies, these are needed by the @backstage/plugin-scaffolder-backend. 15 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ 16 | --mount=type=cache,target=/var/lib/apt,sharing=locked \ 17 | apt-get update && \ 18 | apt-get install -y --no-install-recommends python3 g++ build-essential && \ 19 | yarn config set python /usr/bin/python3 20 | 21 | # Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image, 22 | # in which case you should also move better-sqlite3 to "devDependencies" in package.json. 23 | RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ 24 | --mount=type=cache,target=/var/lib/apt,sharing=locked \ 25 | apt-get update && \ 26 | apt-get install -y --no-install-recommends libsqlite3-dev 27 | 28 | # From here on we use the least-privileged `node` user to run the backend. 29 | USER node 30 | 31 | # This should create the app dir as `node`. 32 | # If it is instead created as `root` then the `tar` command below will fail: `can't create directory 'packages/': Permission denied`. 33 | # If this occurs, then ensure BuildKit is enabled (`DOCKER_BUILDKIT=1`) so the app dir is correctly created as `node`. 34 | WORKDIR /app 35 | 36 | # This switches many Node.js dependencies to production mode. 37 | ENV NODE_ENV production 38 | 39 | # Copy repo skeleton first, to avoid unnecessary docker cache invalidation. 40 | # The skeleton contains the package.json of each package in the monorepo, 41 | # and along with yarn.lock and the root package.json, that's enough to run yarn install. 42 | COPY --chown=node:node yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ 43 | RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz 44 | 45 | RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \ 46 | yarn install --frozen-lockfile --production --network-timeout 300000 47 | 48 | # Then copy the rest of the backend bundle, along with any other files we might want. 49 | COPY --chown=node:node packages/backend/dist/bundle.tar.gz app-config*.yaml ./ 50 | RUN tar xzf bundle.tar.gz && rm bundle.tar.gz 51 | 52 | CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] 53 | -------------------------------------------------------------------------------- /packages/backend/README.md: -------------------------------------------------------------------------------- 1 | # example-backend 2 | 3 | This package is an EXAMPLE of a Backstage backend. 4 | 5 | The main purpose of this package is to provide a test bed for Backstage plugins 6 | that have a backend part. Feel free to experiment locally or within your fork by 7 | adding dependencies and routes to this backend, to try things out. 8 | 9 | Our goal is to eventually amend the create-app flow of the CLI, such that a 10 | production ready version of a backend skeleton is made alongside the frontend 11 | app. Until then, feel free to experiment here! 12 | 13 | ## Development 14 | 15 | To run the example backend, first go to the project root and run 16 | 17 | ```bash 18 | yarn install 19 | ``` 20 | 21 | You should only need to do this once. 22 | 23 | After that, go to the `packages/backend` directory and run 24 | 25 | ```bash 26 | yarn start 27 | ``` 28 | 29 | If you want to override any configuration locally, for example adding any secrets, 30 | you can do so in `app-config.local.yaml`. 31 | 32 | The backend starts up on port 7007 per default. 33 | 34 | ## Populating The Catalog 35 | 36 | If you want to use the catalog functionality, you need to add so called 37 | locations to the backend. These are places where the backend can find some 38 | entity descriptor data to consume and serve. For more information, see 39 | [Software Catalog Overview - Adding Components to the Catalog](https://backstage.io/docs/features/software-catalog/#adding-components-to-the-catalog). 40 | 41 | To get started quickly, this template already includes some statically configured example locations 42 | in `app-config.yaml` under `catalog.locations`. You can remove and replace these locations as you 43 | like, and also override them for local development in `app-config.local.yaml`. 44 | 45 | ## Authentication 46 | 47 | We chose [Passport](http://www.passportjs.org/) as authentication platform due 48 | to its comprehensive set of supported authentication 49 | [strategies](http://www.passportjs.org/packages/). 50 | 51 | Read more about the 52 | [auth-backend](https://github.com/backstage/backstage/blob/master/plugins/auth-backend/README.md) 53 | and 54 | [how to add a new provider](https://github.com/backstage/backstage/blob/master/docs/auth/add-auth-provider.md) 55 | 56 | ## Documentation 57 | 58 | - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) 59 | - [Backstage Documentation](https://backstage.io/docs) 60 | -------------------------------------------------------------------------------- /packages/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "0.0.0", 4 | "main": "dist/index.cjs.js", 5 | "types": "src/index.ts", 6 | "private": true, 7 | "backstage": { 8 | "role": "backend" 9 | }, 10 | "scripts": { 11 | "start": "backstage-cli package start", 12 | "build": "backstage-cli package build", 13 | "lint": "backstage-cli package lint", 14 | "test": "backstage-cli package test", 15 | "clean": "backstage-cli package clean", 16 | "build-image": "docker build ../.. -f Dockerfile --tag backstage" 17 | }, 18 | "dependencies": { 19 | "@backstage/backend-common": "^0.20.1", 20 | "@backstage/backend-tasks": "^0.5.14", 21 | "@backstage/catalog-client": "^1.5.2", 22 | "@backstage/catalog-model": "^1.4.3", 23 | "@backstage/config": "^1.1.1", 24 | "@backstage/plugin-app-backend": "^0.3.57", 25 | "@backstage/plugin-auth-backend": "^0.20.3", 26 | "@backstage/plugin-auth-node": "^0.4.3", 27 | "@backstage/plugin-catalog-backend": "^1.16.1", 28 | "@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "^0.1.6", 29 | "@backstage/plugin-kubernetes-backend": "^0.14.1", 30 | "@backstage/plugin-permission-common": "^0.7.12", 31 | "@backstage/plugin-permission-node": "^0.7.20", 32 | "@backstage/plugin-proxy-backend": "^0.4.7", 33 | "@backstage/plugin-scaffolder-backend": "^1.20.0", 34 | "@backstage/plugin-search-backend": "^1.4.9", 35 | "@backstage/plugin-search-backend-module-catalog": "^0.1.13", 36 | "@backstage/plugin-search-backend-module-pg": "^0.5.18", 37 | "@backstage/plugin-search-backend-module-techdocs": "^0.1.13", 38 | "@backstage/plugin-search-backend-node": "^1.2.13", 39 | "@backstage/plugin-techdocs-backend": "^1.9.2", 40 | "app": "link:../app", 41 | "better-sqlite3": "^9.0.0", 42 | "dockerode": "^3.3.1", 43 | "express": "^4.17.1", 44 | "express-promise-router": "^4.1.0", 45 | "node-gyp": "^9.0.0", 46 | "pg": "^8.11.3", 47 | "winston": "^3.2.1" 48 | }, 49 | "devDependencies": { 50 | "@backstage/cli": "^0.25.1", 51 | "@types/dockerode": "^3.3.0", 52 | "@types/express": "^4.17.6", 53 | "@types/express-serve-static-core": "^4.17.5", 54 | "@types/luxon": "^2.0.4" 55 | }, 56 | "files": [ 57 | "dist" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /packages/backend/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { PluginEnvironment } from './types'; 2 | 3 | describe('test', () => { 4 | it('unbreaks the test runner', () => { 5 | const unbreaker = {} as PluginEnvironment; 6 | expect(unbreaker).toBeTruthy(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /packages/backend/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Hi! 3 | * 4 | * Note that this is an EXAMPLE Backstage backend. Please check the README. 5 | * 6 | * Happy hacking! 7 | */ 8 | 9 | import Router from 'express-promise-router'; 10 | import { 11 | createServiceBuilder, 12 | loadBackendConfig, 13 | getRootLogger, 14 | useHotMemoize, 15 | notFoundHandler, 16 | CacheManager, 17 | DatabaseManager, 18 | HostDiscovery, 19 | UrlReaders, 20 | ServerTokenManager, 21 | } from '@backstage/backend-common'; 22 | import { TaskScheduler } from '@backstage/backend-tasks'; 23 | import { Config } from '@backstage/config'; 24 | import app from './plugins/app'; 25 | import auth from './plugins/auth'; 26 | import catalog from './plugins/catalog'; 27 | import scaffolder from './plugins/scaffolder'; 28 | import proxy from './plugins/proxy'; 29 | import techdocs from './plugins/techdocs'; 30 | import search from './plugins/search'; 31 | import { PluginEnvironment } from './types'; 32 | import { ServerPermissionClient } from '@backstage/plugin-permission-node'; 33 | import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; 34 | import kubernetes from './plugins/kubernetes'; 35 | 36 | function makeCreateEnv(config: Config) { 37 | const root = getRootLogger(); 38 | const reader = UrlReaders.default({ logger: root, config }); 39 | const discovery = HostDiscovery.fromConfig(config); 40 | const cacheManager = CacheManager.fromConfig(config); 41 | const databaseManager = DatabaseManager.fromConfig(config, { logger: root }); 42 | const tokenManager = ServerTokenManager.noop(); 43 | const taskScheduler = TaskScheduler.fromConfig(config, { databaseManager }); 44 | 45 | const identity = DefaultIdentityClient.create({ 46 | discovery, 47 | }); 48 | const permissions = ServerPermissionClient.fromConfig(config, { 49 | discovery, 50 | tokenManager, 51 | }); 52 | 53 | root.info(`Created UrlReader ${reader}`); 54 | 55 | return (plugin: string): PluginEnvironment => { 56 | const logger = root.child({ type: 'plugin', plugin }); 57 | const database = databaseManager.forPlugin(plugin); 58 | const cache = cacheManager.forPlugin(plugin); 59 | const scheduler = taskScheduler.forPlugin(plugin); 60 | return { 61 | logger, 62 | database, 63 | cache, 64 | config, 65 | reader, 66 | discovery, 67 | tokenManager, 68 | scheduler, 69 | permissions, 70 | identity, 71 | }; 72 | }; 73 | } 74 | 75 | async function main() { 76 | const config = await loadBackendConfig({ 77 | argv: process.argv, 78 | logger: getRootLogger(), 79 | }); 80 | const createEnv = makeCreateEnv(config); 81 | 82 | const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); 83 | const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder')); 84 | const authEnv = useHotMemoize(module, () => createEnv('auth')); 85 | const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); 86 | const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs')); 87 | const searchEnv = useHotMemoize(module, () => createEnv('search')); 88 | const appEnv = useHotMemoize(module, () => createEnv('app')); 89 | const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes')); 90 | 91 | const apiRouter = Router(); 92 | apiRouter.use('/catalog', await catalog(catalogEnv)); 93 | apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); 94 | apiRouter.use('/auth', await auth(authEnv)); 95 | apiRouter.use('/techdocs', await techdocs(techdocsEnv)); 96 | apiRouter.use('/proxy', await proxy(proxyEnv)); 97 | apiRouter.use('/search', await search(searchEnv)); 98 | apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv)); 99 | 100 | // Add backends ABOVE this line; this 404 handler is the catch-all fallback 101 | apiRouter.use(notFoundHandler()); 102 | 103 | const service = createServiceBuilder(module) 104 | .loadConfig(config) 105 | .addRouter('/api', apiRouter) 106 | .addRouter('', await app(appEnv)); 107 | 108 | await service.start().catch(err => { 109 | console.log(err); 110 | process.exit(1); 111 | }); 112 | } 113 | 114 | module.hot?.accept(); 115 | main().catch(error => { 116 | console.error('Backend failed to start up', error); 117 | process.exit(1); 118 | }); 119 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/app.ts: -------------------------------------------------------------------------------- 1 | import { createRouter } from '@backstage/plugin-app-backend'; 2 | import { Router } from 'express'; 3 | import { PluginEnvironment } from '../types'; 4 | 5 | export default async function createPlugin( 6 | env: PluginEnvironment, 7 | ): Promise { 8 | return await createRouter({ 9 | logger: env.logger, 10 | config: env.config, 11 | database: env.database, 12 | appPackageName: 'app', 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/auth.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createRouter, 3 | providers, 4 | defaultAuthProviderFactories, 5 | } from '@backstage/plugin-auth-backend'; 6 | import { Router } from 'express'; 7 | import { PluginEnvironment } from '../types'; 8 | 9 | export default async function createPlugin( 10 | env: PluginEnvironment, 11 | ): Promise { 12 | return await createRouter({ 13 | logger: env.logger, 14 | config: env.config, 15 | database: env.database, 16 | discovery: env.discovery, 17 | tokenManager: env.tokenManager, 18 | providerFactories: { 19 | ...defaultAuthProviderFactories, 20 | 21 | // This replaces the default GitHub auth provider with a customized one. 22 | // The `signIn` option enables sign-in for this provider, using the 23 | // identity resolution logic that's provided in the `resolver` callback. 24 | // 25 | // This particular resolver makes all users share a single "guest" identity. 26 | // It should only be used for testing and trying out Backstage. 27 | // 28 | // If you want to use a production ready resolver you can switch to 29 | // the one that is commented out below, it looks up a user entity in the 30 | // catalog using the GitHub username of the authenticated user. 31 | // That resolver requires you to have user entities populated in the catalog, 32 | // for example using https://backstage.io/docs/integrations/github/org 33 | // 34 | // There are other resolvers to choose from, and you can also create 35 | // your own, see the auth documentation for more details: 36 | // 37 | // https://backstage.io/docs/auth/identity-resolver 38 | github: providers.github.create({ 39 | signIn: { 40 | resolver(_, ctx) { 41 | const userRef = 'user:default/guest'; // Must be a full entity reference 42 | return ctx.issueToken({ 43 | claims: { 44 | sub: userRef, // The user's own identity 45 | ent: [userRef], // A list of identities that the user claims ownership through 46 | }, 47 | }); 48 | }, 49 | // resolver: providers.github.resolvers.usernameMatchingUserEntityName(), 50 | }, 51 | }), 52 | }, 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/catalog.ts: -------------------------------------------------------------------------------- 1 | import { CatalogBuilder } from '@backstage/plugin-catalog-backend'; 2 | import { ScaffolderEntitiesProcessor } from '@backstage/plugin-catalog-backend-module-scaffolder-entity-model'; 3 | import { Router } from 'express'; 4 | import { PluginEnvironment } from '../types'; 5 | 6 | export default async function createPlugin( 7 | env: PluginEnvironment, 8 | ): Promise { 9 | const builder = await CatalogBuilder.create(env); 10 | builder.addProcessor(new ScaffolderEntitiesProcessor()); 11 | const { processingEngine, router } = await builder.build(); 12 | await processingEngine.start(); 13 | return router; 14 | } 15 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/kubernetes.ts: -------------------------------------------------------------------------------- 1 | import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend'; 2 | import { Router } from 'express'; 3 | import { PluginEnvironment } from '../types'; 4 | import { CatalogClient } from '@backstage/catalog-client'; 5 | 6 | export default async function createPlugin( 7 | env: PluginEnvironment, 8 | ): Promise { 9 | const catalogApi = new CatalogClient({ discoveryApi: env.discovery }); 10 | const { router } = await KubernetesBuilder.createBuilder({ 11 | logger: env.logger, 12 | config: env.config, 13 | catalogApi, 14 | permissions: env.permissions, 15 | }).build(); 16 | return router; 17 | } -------------------------------------------------------------------------------- /packages/backend/src/plugins/proxy.ts: -------------------------------------------------------------------------------- 1 | import { createRouter } from '@backstage/plugin-proxy-backend'; 2 | import { Router } from 'express'; 3 | import { PluginEnvironment } from '../types'; 4 | 5 | export default async function createPlugin( 6 | env: PluginEnvironment, 7 | ): Promise { 8 | return await createRouter({ 9 | logger: env.logger, 10 | config: env.config, 11 | discovery: env.discovery, 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/scaffolder.ts: -------------------------------------------------------------------------------- 1 | import { CatalogClient } from '@backstage/catalog-client'; 2 | import { createRouter } from '@backstage/plugin-scaffolder-backend'; 3 | import { Router } from 'express'; 4 | import type { PluginEnvironment } from '../types'; 5 | 6 | export default async function createPlugin( 7 | env: PluginEnvironment, 8 | ): Promise { 9 | const catalogClient = new CatalogClient({ 10 | discoveryApi: env.discovery, 11 | }); 12 | 13 | return await createRouter({ 14 | logger: env.logger, 15 | config: env.config, 16 | database: env.database, 17 | reader: env.reader, 18 | catalogClient, 19 | identity: env.identity, 20 | permissions: env.permissions, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/search.ts: -------------------------------------------------------------------------------- 1 | import { useHotCleanup } from '@backstage/backend-common'; 2 | import { createRouter } from '@backstage/plugin-search-backend'; 3 | import { 4 | IndexBuilder, 5 | LunrSearchEngine, 6 | } from '@backstage/plugin-search-backend-node'; 7 | import { PluginEnvironment } from '../types'; 8 | import { DefaultCatalogCollatorFactory } from '@backstage/plugin-search-backend-module-catalog'; 9 | import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-search-backend-module-techdocs'; 10 | import { Router } from 'express'; 11 | 12 | export default async function createPlugin( 13 | env: PluginEnvironment, 14 | ): Promise { 15 | // Initialize a connection to a search engine. 16 | const searchEngine = new LunrSearchEngine({ 17 | logger: env.logger, 18 | }); 19 | const indexBuilder = new IndexBuilder({ 20 | logger: env.logger, 21 | searchEngine, 22 | }); 23 | 24 | const schedule = env.scheduler.createScheduledTaskRunner({ 25 | frequency: { minutes: 10 }, 26 | timeout: { minutes: 15 }, 27 | // A 3 second delay gives the backend server a chance to initialize before 28 | // any collators are executed, which may attempt requests against the API. 29 | initialDelay: { seconds: 3 }, 30 | }); 31 | 32 | // Collators are responsible for gathering documents known to plugins. This 33 | // collator gathers entities from the software catalog. 34 | indexBuilder.addCollator({ 35 | schedule, 36 | factory: DefaultCatalogCollatorFactory.fromConfig(env.config, { 37 | discovery: env.discovery, 38 | tokenManager: env.tokenManager, 39 | }), 40 | }); 41 | 42 | // collator gathers entities from techdocs. 43 | indexBuilder.addCollator({ 44 | schedule, 45 | factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, { 46 | discovery: env.discovery, 47 | logger: env.logger, 48 | tokenManager: env.tokenManager, 49 | }), 50 | }); 51 | 52 | // The scheduler controls when documents are gathered from collators and sent 53 | // to the search engine for indexing. 54 | const { scheduler } = await indexBuilder.build(); 55 | scheduler.start(); 56 | 57 | useHotCleanup(module, () => scheduler.stop()); 58 | 59 | return await createRouter({ 60 | engine: indexBuilder.getSearchEngine(), 61 | types: indexBuilder.getDocumentTypes(), 62 | permissions: env.permissions, 63 | config: env.config, 64 | logger: env.logger, 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /packages/backend/src/plugins/techdocs.ts: -------------------------------------------------------------------------------- 1 | import { DockerContainerRunner } from '@backstage/backend-common'; 2 | import { 3 | createRouter, 4 | Generators, 5 | Preparers, 6 | Publisher, 7 | } from '@backstage/plugin-techdocs-backend'; 8 | import Docker from 'dockerode'; 9 | import { Router } from 'express'; 10 | import { PluginEnvironment } from '../types'; 11 | 12 | export default async function createPlugin( 13 | env: PluginEnvironment, 14 | ): Promise { 15 | // Preparers are responsible for fetching source files for documentation. 16 | const preparers = await Preparers.fromConfig(env.config, { 17 | logger: env.logger, 18 | reader: env.reader, 19 | }); 20 | 21 | // Docker client (conditionally) used by the generators, based on techdocs.generators config. 22 | const dockerClient = new Docker(); 23 | const containerRunner = new DockerContainerRunner({ dockerClient }); 24 | 25 | // Generators are used for generating documentation sites. 26 | const generators = await Generators.fromConfig(env.config, { 27 | logger: env.logger, 28 | containerRunner, 29 | }); 30 | 31 | // Publisher is used for 32 | // 1. Publishing generated files to storage 33 | // 2. Fetching files from storage and passing them to TechDocs frontend. 34 | const publisher = await Publisher.fromConfig(env.config, { 35 | logger: env.logger, 36 | discovery: env.discovery, 37 | }); 38 | 39 | // checks if the publisher is working and logs the result 40 | await publisher.getReadiness(); 41 | 42 | return await createRouter({ 43 | preparers, 44 | generators, 45 | publisher, 46 | logger: env.logger, 47 | config: env.config, 48 | discovery: env.discovery, 49 | cache: env.cache, 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /packages/backend/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Logger } from 'winston'; 2 | import { Config } from '@backstage/config'; 3 | import { 4 | PluginCacheManager, 5 | PluginDatabaseManager, 6 | PluginEndpointDiscovery, 7 | TokenManager, 8 | UrlReader, 9 | } from '@backstage/backend-common'; 10 | import { PluginTaskScheduler } from '@backstage/backend-tasks'; 11 | import { PermissionEvaluator } from '@backstage/plugin-permission-common'; 12 | import { IdentityApi } from '@backstage/plugin-auth-node'; 13 | 14 | export type PluginEnvironment = { 15 | logger: Logger; 16 | database: PluginDatabaseManager; 17 | cache: PluginCacheManager; 18 | config: Config; 19 | reader: UrlReader; 20 | discovery: PluginEndpointDiscovery; 21 | tokenManager: TokenManager; 22 | scheduler: PluginTaskScheduler; 23 | permissions: PermissionEvaluator; 24 | identity: IdentityApi; 25 | }; 26 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 The Backstage 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 | 17 | import { defineConfig } from '@playwright/test'; 18 | import { generateProjects } from '@backstage/e2e-test-utils/playwright'; 19 | 20 | /** 21 | * See https://playwright.dev/docs/test-configuration. 22 | */ 23 | export default defineConfig({ 24 | timeout: 60_000, 25 | 26 | expect: { 27 | timeout: 5_000, 28 | }, 29 | 30 | // Run your local dev server before starting the tests 31 | webServer: process.env.CI 32 | ? [] 33 | : [ 34 | { 35 | command: 'yarn start', 36 | port: 3000, 37 | reuseExistingServer: true, 38 | timeout: 60_000, 39 | }, 40 | ], 41 | 42 | forbidOnly: !!process.env.CI, 43 | 44 | retries: process.env.CI ? 2 : 0, 45 | 46 | reporter: [['html', { open: 'never', outputFolder: 'e2e-test-report' }]], 47 | 48 | use: { 49 | actionTimeout: 0, 50 | baseURL: 51 | process.env.PLAYWRIGHT_URL ?? 52 | (process.env.CI ? 'http://localhost:7007' : 'http://localhost:3000'), 53 | screenshot: 'only-on-failure', 54 | trace: 'on-first-retry', 55 | }, 56 | 57 | outputDir: 'node_modules/.cache/e2e-test-results', 58 | 59 | projects: generateProjects(), // Find all packages with e2e-test folders 60 | }); 61 | -------------------------------------------------------------------------------- /plugins/README.md: -------------------------------------------------------------------------------- 1 | # The Plugins Folder 2 | 3 | This is where your own plugins and their associated modules live, each in a 4 | separate folder of its own. 5 | 6 | If you want to create a new plugin here, go to your project root directory, run 7 | the command `yarn new`, and follow the on-screen instructions. 8 | 9 | You can also check out existing plugins on [the plugin marketplace](https://backstage.io/plugins)! 10 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('@backstage/cli/config/eslint-factory')(__dirname), 3 | rules: { 4 | ...require('@backstage/cli/config/eslint-factory')(__dirname).rules, 5 | 'no-nested-ternary': 'off', 6 | } 7 | } -------------------------------------------------------------------------------- /plugins/tekton-pipelines/README.md: -------------------------------------------------------------------------------- 1 | # tekton-pipelines-plugin 2 | 3 | This is the Tekton Pipelines frontend plugin. 4 | 5 | _This plugin was created through the Backstage CLI_ 6 | 7 | ## Getting started 8 | 9 | Access the frontend plugin by running `yarn start` in the root directory of the plugin, and then navigate to /tekton-pipelines. 10 | 11 | This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. It is only meant for local development, and the setup for it can be found inside the /dev directory. 12 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/dev/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevApp } from '@backstage/dev-utils'; 3 | import { EntityTektonPipelinesContent, tektonPipelinesPluginPlugin } from '../src/plugin'; 4 | import { KubernetesApi, kubernetesApiRef, kubernetesPlugin} from '@backstage/plugin-kubernetes'; 5 | import { 6 | CustomObjectsByEntityRequest, 7 | FetchResponse, 8 | ObjectsByEntityResponse, 9 | WorkloadsByEntityRequest, 10 | } from '@backstage/plugin-kubernetes-common'; 11 | 12 | import pipelineRun1 from '../src/__fixtures__/pipelineRun1.json'; 13 | import pipelineRun2 from '../src/__fixtures__/pipelineRun2.json'; 14 | import pipelineRun3 from '../src/__fixtures__/pipelineRun3.json'; 15 | import { TestApiProvider } from '@backstage/test-utils'; 16 | import { EntityProvider } from '@backstage/plugin-catalog-react'; 17 | import { Entity } from '@backstage/catalog-model'; 18 | 19 | 20 | const mockEntity: Entity = { 21 | apiVersion: 'backstage.io/v1alpha1', 22 | kind: 'Component', 23 | metadata: { 24 | name: 'backstage', 25 | description: 'backstage.io', 26 | annotations: { 27 | 'backstage.io/kubernetes-label-selector': 'app=microservice', 28 | 'tektonci/enabled': "true", 29 | }, 30 | }, 31 | spec: { 32 | lifecycle: 'production', 33 | type: 'service', 34 | owner: 'user:guest', 35 | }, 36 | }; 37 | 38 | class MockKubernetesClient implements KubernetesApi { 39 | readonly resources: FetchResponse[]; 40 | 41 | constructor(fixtureData: { [resourceType: string]: any[] }) { 42 | this.resources = Object.entries(fixtureData).flatMap( 43 | ([type, resources]) => 44 | ({ type: type.toLocaleLowerCase('en-US'), resources } as FetchResponse), 45 | ); 46 | } 47 | getCluster(_clusterName: string): Promise<{ name: string; authProvider: string; oidcTokenProvider?: string | undefined; dashboardUrl?: string | undefined; } | undefined> { 48 | throw new Error('Method not implemented.'); 49 | } 50 | async getPodLogs(_request: { 51 | podName: string; 52 | namespace: string; 53 | clusterName: string; 54 | containerName: string; 55 | token: string; 56 | }): Promise { 57 | return await 'some logs'; 58 | } 59 | async getWorkloadsByEntity( 60 | _request: WorkloadsByEntityRequest, 61 | ): Promise { 62 | return { 63 | items: [ 64 | { 65 | cluster: { name: 'mock-cluster' }, 66 | resources: this.resources, 67 | podMetrics: [], 68 | errors: [], 69 | }, 70 | ], 71 | }; 72 | } 73 | async getCustomObjectsByEntity( 74 | _request: CustomObjectsByEntityRequest, 75 | ): Promise { 76 | return { 77 | items: [ 78 | { 79 | cluster: { name: 'mock-cluster' }, 80 | resources: this.resources, 81 | podMetrics: [], 82 | errors: [], 83 | }, 84 | ], 85 | }; 86 | } 87 | 88 | async getObjectsByEntity(): Promise { 89 | return { 90 | items: [ 91 | { 92 | cluster: { name: 'mock-cluster' }, 93 | resources: this.resources, 94 | podMetrics: [], 95 | errors: [], 96 | }, 97 | ], 98 | }; 99 | } 100 | 101 | async getClusters(): Promise<{ name: string; authProvider: string }[]> { 102 | return [{ name: 'mock-cluster', authProvider: 'serviceAccount' }]; 103 | } 104 | 105 | async proxy(_options: { clusterName: String; path: String }): Promise { 106 | return { 107 | kind: 'Namespace', 108 | apiVersion: 'v1', 109 | metadata: { 110 | name: 'mock-ns', 111 | }, 112 | }; 113 | } 114 | } 115 | 116 | createDevApp() 117 | .registerPlugin(kubernetesPlugin, tektonPipelinesPluginPlugin) 118 | .addPage({ 119 | element: ( 120 | 123 | 124 | 125 | 126 | 127 | ), 128 | title: 'Tekton Pipelines 1', 129 | path: '/tekton-pipelines-1' 130 | }).addPage({ 131 | element: ( 132 | 135 | 136 | 137 | 138 | 139 | ), 140 | title: 'Tekton Pipelines 2', 141 | path: '/tekton-pipelines-2' 142 | }).addPage({ 143 | element: ( 144 | 147 | 148 | 149 | 150 | 151 | ), 152 | title: 'Tekton Pipelines 3', 153 | path: '/tekton-pipelines-3' 154 | }) 155 | .render(); 156 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jquad-group/backstage-plugin-tekton-pipelines-plugin", 3 | "version": "1.1.0-beta.4", 4 | "main": "src/index.ts", 5 | "types": "src/index.ts", 6 | "license": "Apache-2.0", 7 | "private": false, 8 | "publishConfig": { 9 | "access": "public", 10 | "main": "dist/index.esm.js", 11 | "types": "dist/index.d.ts" 12 | }, 13 | "backstage": { 14 | "role": "frontend-plugin" 15 | }, 16 | "sideEffects": false, 17 | "scripts": { 18 | "start": "backstage-cli package start", 19 | "build": "backstage-cli package build", 20 | "lint": "backstage-cli package lint", 21 | "test": "backstage-cli package test", 22 | "clean": "backstage-cli package clean", 23 | "prepack": "backstage-cli package prepack", 24 | "postpack": "backstage-cli package postpack" 25 | }, 26 | "dependencies": { 27 | "@backstage/catalog-model": "^1.4.3", 28 | "@backstage/core-components": "^0.13.10", 29 | "@backstage/core-plugin-api": "^1.8.2", 30 | "@backstage/plugin-catalog-react": "^1.9.3", 31 | "@backstage/plugin-kubernetes": "^0.11.4", 32 | "@backstage/theme": "^0.5.0", 33 | "@material-ui/core": "^4.9.13", 34 | "@material-ui/icons": "^4.9.1", 35 | "@material-ui/lab": "^4.0.0-alpha.57", 36 | "react-digraph": "^9.1.1", 37 | "react-use": "^17.2.4" 38 | }, 39 | "peerDependencies": { 40 | "react": "^16.13.1 || ^17.0.0" 41 | }, 42 | "devDependencies": { 43 | "@backstage/cli": "^0.25.1", 44 | "@backstage/core-app-api": "^1.11.3", 45 | "@backstage/dev-utils": "^1.0.26", 46 | "@backstage/plugin-kubernetes-common": "^0.7.3", 47 | "@backstage/test-utils": "^1.4.7", 48 | "@testing-library/jest-dom": "^5.10.1", 49 | "@testing-library/react": "^12.1.3", 50 | "@testing-library/user-event": "^14.0.0", 51 | "msw": "^1.0.0" 52 | }, 53 | "files": [ 54 | "dist" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/Router.tsx: -------------------------------------------------------------------------------- 1 | import { Entity } from '@backstage/catalog-model'; 2 | import { 3 | LinkButton, 4 | MissingAnnotationEmptyState, 5 | } from '@backstage/core-components'; 6 | import { useEntity } from '@backstage/plugin-catalog-react'; 7 | import React from 'react'; 8 | import { Route, Routes } from 'react-router-dom'; 9 | import { TektonDashboardComponent } from './components/TektonDashboard'; 10 | 11 | export const TEKTON_PIPELINES_ANNOTATION = 'tektonci/enabled'; 12 | 13 | export const isTektonCiAvailable = (entity: Entity) => 14 | Boolean(entity?.metadata.annotations?.[TEKTON_PIPELINES_ANNOTATION]); 15 | 16 | export const Router = (props: { refreshIntervalMs?: number }) => { 17 | const { entity } = useEntity(); 18 | 19 | const tektonPipelinesAnnotationValue = 20 | entity.metadata.annotations?.[TEKTON_PIPELINES_ANNOTATION]; 21 | 22 | if ( 23 | tektonPipelinesAnnotationValue 24 | ) { 25 | return ( 26 | 27 | 28 | 35 | } 36 | /> 37 | 38 | 39 | ); 40 | } 41 | 42 | return ( 43 | <> 44 | 47 | 48 | 54 | Read Tekton Pipelines Plugin Docs 55 | 56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/__fixtures__/log.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": "Skip to main content Skip to main content Skip to main content Skip to main content Skip to main content Skip to main content Skip to main content Skip to main content Skip to main content Skip to main content Skip to main content Skip to main content Skip to main content \n \n mkdir -p /workspace/repo/bin\n test -s /workspace/repo/bin/controller-gen || GOBIN=/workspace/repo/bin go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.10.0\n go: downloading sigs.k8s.io/controller-tools v0.10.0\n go: downloading github.com/spf13/cobra v1.4.0\n go: downloading github.com/spf13/pflag v1.0.5\n go: downloading github.com/gobuffalo/flect v0.2.5\n go: downloading k8s.io/apiextensions-apiserver v0.25.0\n go: downloading k8s.io/apimachinery v0.25.0\n go: downloading golang.org/x/tools v0.1.12\n go: downloading gopkg.in/yaml.v2 v2.4.0\n go: downloading github.com/fatih/color v1.12.0\n go: downloading k8s.io/api v0.25.0\n go: downloading gopkg.in/yaml.v3 v3.0.1\n go: downloading sigs.k8s.io/yaml v1.3.0\n go: downloading github.com/gogo/protobuf v1.3.2\n go: downloading k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed\n go: downloading k8s.io/klog/v2 v2.70.1\n go: downloading sigs.k8s.io/structured-merge-diff/v4 v4.2.3\n go: downloading github.com/google/gofuzz v1.1.0\n go: downloading github.com/mattn/go-colorable v0.1.8\n go: downloading github.com/mattn/go-isatty v0.0.12\n go: downloading golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f\n go: downloading sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2\n go: downloading github.com/go-logr/logr v1.2.3\n go: downloading gopkg.in/inf.v0 v0.9.1\n go: downloading github.com/json-iterator/go v1.1.12\n go: downloading golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4\n go: downloading golang.org/x/net v0.0.0-20220722155237-a158d28d115b\n go: downloading github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd\n go: downloading github.com/modern-go/reflect2 v1.0.2\n go: downloading golang.org/x/text v0.3.7\n /workspace/repo/bin/controller-gen rbac:roleName=manager-role crd webhook paths=\"./...\" output:crd:artifacts:config=config/crd/bases\n /workspace/repo/bin/controller-gen object:headerFile=\"hack/boilerplate.go.txt\" paths\"./...\"\n go fmt ./...\n go vet ./...\n go: downloading github.com/onsi/ginkgo/v2 v2.5.1\n go: downloading github.com/onsi/gomega v1.24.0\n go: downloading github.com/onsi/ginkgo v1.16.5\n test -s /workspace/repo/bin/setup-envtest || GOBIN=/workspace/repo/bin go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest\n go: downloading sigs.k8s.io/controller-runtime v0.14.5\n go: downloading sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20230307042619-c304e7ec2ee7\n go: downloading github.com/go-logr/logr v1.2.0\n go: downloading github.com/go-logr/zapr v1.2.0\n go: downloading github.com/spf13/afero v1.6.0\n go: downloading go.uber.org/zap v1.19.1\n go: downloading go.uber.org/atomic v1.7.0\n go: downloading go.uber.org/multierr v1.6.0\n KUBEBUILDER_ASSETS=\"/root/.local/share/kubebuilder-envtest/k8s/1.23.1-linux-amd64\" go test ./... -coverprofile cover.out\n ? github.com/jquad-group/pullrequest-operator [no test files]\n ? github.com/jquad-group/pullrequest-operator/api/v1alpha1 [no test files]\n ok github.com/jquad-group/pullrequest-operator/controllers 0.047s coverage: 0.0% of statements\n ? github.com/jquad-group/pullrequest-operator/pkg/git [no test files]\n \n Step completed successfully\n \n fix-gh-client-cz7jw\n Last updated 26 days ago\n CompletedTasks Completed: 4 (Failed: 0, Cancelled 0), Skipped: 1\n \n clone\n github-commit-status-pending\n build-and-test-task-go\n \n test\n \n build\n \n github-commit-status-error\n github-commit-status-success\n testCompleted\n Duration: 4m 58s\n" 3 | } -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/CollapsibleTable/CollapsibleTable.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, waitFor } from '@testing-library/react'; 3 | import { wrapInTestApp } from '@backstage/test-utils'; 4 | import { CollapsibleTable } from './CollapsibleTable'; 5 | import * as pipelineRunFileMock from './__fixtures__/pipelinerun.json'; 6 | 7 | describe('CollapsibleTable', () => { 8 | it('should render a pipelinerun', async () => { 9 | const { getByText, debug } = render( 10 | wrapInTestApp( 11 | , 12 | ), 13 | ); 14 | 15 | await waitFor(() => { 16 | debug(); 17 | }); 18 | 19 | expect(getByText('feature-added-catalog-info-xdjk9')).toBeInTheDocument(); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/CollapsibleTable/CollapsibleTable.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // eslint-disable-next-line no-restricted-imports 3 | import { Table, TableContainer, TableBody, TableRow, TableCell, TableHead, TablePagination, TableFooter } from '@material-ui/core'; 4 | /* ignore lint error for internal dependencies */ 5 | /* eslint-disable */ 6 | import { PipelineRun } from '../../types'; 7 | /* eslint-enable */ 8 | import TablePaginationActions from '@material-ui/core/TablePagination/TablePaginationActions'; 9 | import { CollapsibleTableRow } from '../CollapsibleTableRow'; 10 | 11 | type PipelineRunProps = { 12 | clusterName?: string; 13 | pipelineruns?: PipelineRun[]; 14 | }; 15 | 16 | export const CollapsibleTable = ({ clusterName, pipelineruns }: PipelineRunProps) => { 17 | const [page, setPage] = React.useState(0); 18 | const [rowsPerPage, setRowsPerPage] = React.useState(5); 19 | let emptyRows: number; 20 | // Avoid a layout jump when reaching the last page with empty rows. 21 | if (pipelineruns !== undefined) { 22 | emptyRows = 23 | page > 0 ? Math.max(0, (1 + page) * rowsPerPage - pipelineruns.length) : 0; 24 | } else { 25 | emptyRows = 26 | page > 0 ? Math.max(0, (1 + page) * rowsPerPage) : 0; 27 | } 28 | 29 | const handleChangePage = ( 30 | _event: React.MouseEvent | null, 31 | newPage: number, 32 | ) => { 33 | setPage(newPage); 34 | }; 35 | 36 | const handleChangeRowsPerPage = ( 37 | event: React.ChangeEvent, 38 | ) => { 39 | setRowsPerPage(parseInt(event.target.value, 10)); 40 | setPage(0); 41 | }; 42 | 43 | return ( 44 | 45 | 46 | 47 | 48 | 49 | Name 50 | Namespace 51 | Status 52 | Start Time 53 | Completion Time 54 | Dashboard 55 | 56 | 57 | 58 | {(pipelineruns !== undefined) && (clusterName !== undefined) && (rowsPerPage > 0 59 | ? pipelineruns.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) 60 | : pipelineruns 61 | ).map((pipelineRun) => ( 62 | 63 | ))} 64 | {emptyRows > 0 && ( 65 | 66 | 67 | 68 | )} 69 | 70 | 71 | { pipelineruns !== undefined && 72 | 88 | 89 | } 90 | 91 |
92 |
93 | ); 94 | } 95 | 96 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/CollapsibleTable/__fixtures__/pipelinerun.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiVersion": "tekton.dev/v1beta1", 3 | "kind": "PipelineRun", 4 | "metadata": { 5 | "creationTimestamp": "2022-10-25T18:41:36Z", 6 | "generateName": "feature-added-catalog-info-", 7 | "generation": 1, 8 | "labels": { 9 | "kustomize.toolkit.fluxcd.io/name": "build-pipeline", 10 | "kustomize.toolkit.fluxcd.io/namespace": "sample-go-application-build", 11 | "pipeline.jquad.rocks/pr.branch.commit": "ab43364d1c192228832068e93c0a1357412e48af", 12 | "pipeline.jquad.rocks/pr.branch.name": "feature-added-catalog-info", 13 | "tekton.dev/pipeline": "pullrequest-pipeline-go" 14 | }, 15 | "name": "feature-added-catalog-info-xdjk9", 16 | "namespace": "sample-go-application-build", 17 | "ownerReferences": [ 18 | { 19 | "apiVersion": "pipeline.jquad.rocks/v1alpha1", 20 | "blockOwnerDeletion": true, 21 | "controller": true, 22 | "kind": "PipelineTrigger", 23 | "name": "pipelinetrigger-for-sample-go-application", 24 | "uid": "81601ec0-c693-41c3-9605-4cfa5bf84f2f" 25 | } 26 | ], 27 | "resourceVersion": "142055452", 28 | "uid": "c8abf18e-ab08-4b09-af49-94c7d287b96a" 29 | }, 30 | "spec": { 31 | "params": [ 32 | { 33 | "name": "repositoryName", 34 | "value": "sample-go-application" 35 | }, 36 | { 37 | "name": "pathToContext", 38 | "value": "/workspace/repo" 39 | }, 40 | { 41 | "name": "commit", 42 | "value": "" 43 | }, 44 | { 45 | "name": "branch-name", 46 | "value": "feature/added-catalog-info" 47 | }, 48 | { 49 | "name": "repo-url", 50 | "value": "git@github.com:jquad-group/sample-go-application.git" 51 | }, 52 | { 53 | "name": "repo-url-alternate", 54 | "value": "git@github.com:jquad-group/sample-go-application.git" 55 | }, 56 | { 57 | "name": "gitrevision", 58 | "value": "feature/added-catalog-info" 59 | }, 60 | { 61 | "name": "projectname", 62 | "value": "sample-go-application" 63 | }, 64 | { 65 | "name": "statusUrl", 66 | "value": "https://api.github.com/repos/jquad-group/sample-go-application/statuses/ab43364d1c192228832068e93c0a1357412e48af" 67 | }, 68 | { 69 | "name": "state", 70 | "value": "success" 71 | }, 72 | { 73 | "name": "targetUrl", 74 | "value": "https://rancher.jquad.rocks/k8s/clusters/local/api/v1/namespaces/tekton-pipelines/services/http:tekton-dashboard:9097/proxy/#/pipelineruns" 75 | }, 76 | { 77 | "name": "context", 78 | "value": "jquad-group/tekton-ci" 79 | }, 80 | { 81 | "name": "githubSecretName", 82 | "value": "git-clone" 83 | }, 84 | { 85 | "name": "githubUsernameKey", 86 | "value": "username" 87 | }, 88 | { 89 | "name": "githubPasswordKey", 90 | "value": "password" 91 | } 92 | ], 93 | "pipelineRef": { 94 | "name": "pullrequest-pipeline-go" 95 | }, 96 | "podTemplate": { 97 | "securityContext": { 98 | "fsGroup": 0, 99 | "runAsGroup": 0, 100 | "runAsUser": 0 101 | } 102 | }, 103 | "serviceAccountName": "build-robot", 104 | "timeout": "1h0m0s", 105 | "workspaces": [ 106 | { 107 | "name": "workspace", 108 | "volumeClaimTemplate": { 109 | "metadata": { 110 | "creationTimestamp": null 111 | }, 112 | "spec": { 113 | "accessModes": [ 114 | "ReadWriteOnce" 115 | ], 116 | "resources": { 117 | "requests": { 118 | "storage": "1Gi" 119 | } 120 | } 121 | }, 122 | "status": {} 123 | } 124 | } 125 | ] 126 | }, 127 | "status": { 128 | "completionTime": "2022-10-25T18:42:30Z", 129 | "conditions": [ 130 | { 131 | "lastTransitionTime": "2022-10-25T18:42:30Z", 132 | "message": "Tasks Completed: 4 (Failed: 0, Cancelled 0), Skipped: 1", 133 | "reason": "Completed", 134 | "status": "True", 135 | "type": "Succeeded" 136 | } 137 | ], 138 | "finallyStartTime": "2022-10-25T18:42:22Z", 139 | "pipelineSpec": { 140 | "finally": [ 141 | { 142 | "name": "github-commit-status-error", 143 | "params": [ 144 | { 145 | "name": "statusUrl", 146 | "value": "https://api.github.com/repos/jquad-group/sample-go-application/statuses/ab43364d1c192228832068e93c0a1357412e48af" 147 | }, 148 | { 149 | "name": "description", 150 | "value": "The build has failed." 151 | }, 152 | { 153 | "name": "state", 154 | "value": "error" 155 | }, 156 | { 157 | "name": "targetUrl", 158 | "value": "https://rancher.jquad.rocks/k8s/clusters/local/api/v1/namespaces/tekton-pipelines/services/http:tekton-dashboard:9097/proxy/#/pipelineruns" 159 | }, 160 | { 161 | "name": "context", 162 | "value": "jquad-group/tekton-ci" 163 | }, 164 | { 165 | "name": "secretName", 166 | "value": "git-clone" 167 | }, 168 | { 169 | "name": "usernameKey", 170 | "value": "username" 171 | }, 172 | { 173 | "name": "passwordKey", 174 | "value": "password" 175 | } 176 | ], 177 | "taskRef": { 178 | "kind": "Task", 179 | "name": "github-commit-status" 180 | }, 181 | "when": [ 182 | { 183 | "input": "$(tasks.build-and-test-task-go.status)", 184 | "operator": "in", 185 | "values": [ 186 | "Failed" 187 | ] 188 | } 189 | ] 190 | }, 191 | { 192 | "name": "github-commit-status-success", 193 | "params": [ 194 | { 195 | "name": "statusUrl", 196 | "value": "https://api.github.com/repos/jquad-group/sample-go-application/statuses/ab43364d1c192228832068e93c0a1357412e48af" 197 | }, 198 | { 199 | "name": "description", 200 | "value": "The build was successfull." 201 | }, 202 | { 203 | "name": "state", 204 | "value": "success" 205 | }, 206 | { 207 | "name": "targetUrl", 208 | "value": "https://rancher.jquad.rocks/k8s/clusters/local/api/v1/namespaces/tekton-pipelines/services/http:tekton-dashboard:9097/proxy/#/pipelineruns" 209 | }, 210 | { 211 | "name": "context", 212 | "value": "jquad-group/tekton-ci" 213 | }, 214 | { 215 | "name": "secretName", 216 | "value": "git-clone" 217 | }, 218 | { 219 | "name": "usernameKey", 220 | "value": "username" 221 | }, 222 | { 223 | "name": "passwordKey", 224 | "value": "password" 225 | } 226 | ], 227 | "taskRef": { 228 | "kind": "Task", 229 | "name": "github-commit-status" 230 | }, 231 | "when": [ 232 | { 233 | "input": "$(tasks.build-and-test-task-go.status)", 234 | "operator": "in", 235 | "values": [ 236 | "Succeeded" 237 | ] 238 | } 239 | ] 240 | } 241 | ], 242 | "params": [ 243 | { 244 | "name": "repo-url", 245 | "type": "string" 246 | }, 247 | { 248 | "default": "", 249 | "description": "The git repository URL to clone from.", 250 | "name": "repo-url-alternate", 251 | "type": "string" 252 | }, 253 | { 254 | "description": "The git branch to clone.", 255 | "name": "branch-name", 256 | "type": "string" 257 | }, 258 | { 259 | "description": "the git project name", 260 | "name": "projectname", 261 | "type": "string" 262 | }, 263 | { 264 | "description": "the name of the image", 265 | "name": "repositoryName", 266 | "type": "string" 267 | }, 268 | { 269 | "description": "The build context used by Kaniko", 270 | "name": "pathToContext", 271 | "type": "string" 272 | }, 273 | { 274 | "default": "", 275 | "description": "subdirectory", 276 | "name": "subdirectory", 277 | "type": "string" 278 | }, 279 | { 280 | "name": "statusUrl", 281 | "type": "string" 282 | }, 283 | { 284 | "default": "pending", 285 | "name": "statePending", 286 | "type": "string" 287 | }, 288 | { 289 | "default": "The build was successfull.", 290 | "name": "descriptionSuccess", 291 | "type": "string" 292 | }, 293 | { 294 | "default": "success", 295 | "name": "stateSuccess", 296 | "type": "string" 297 | }, 298 | { 299 | "default": "error", 300 | "name": "stateError", 301 | "type": "string" 302 | }, 303 | { 304 | "default": "The build has failed.", 305 | "name": "descriptionError", 306 | "type": "string" 307 | }, 308 | { 309 | "default": "The build is currently running.", 310 | "name": "descriptionPending", 311 | "type": "string" 312 | }, 313 | { 314 | "default": "https://rancher.jquad.rocks/pipeline", 315 | "name": "targetUrl", 316 | "type": "string" 317 | }, 318 | { 319 | "default": "jquad-group/tekton-ci", 320 | "name": "context", 321 | "type": "string" 322 | }, 323 | { 324 | "name": "githubSecretName", 325 | "type": "string" 326 | }, 327 | { 328 | "name": "githubUsernameKey", 329 | "type": "string" 330 | }, 331 | { 332 | "name": "githubPasswordKey", 333 | "type": "string" 334 | } 335 | ], 336 | "tasks": [ 337 | { 338 | "name": "clone", 339 | "params": [ 340 | { 341 | "name": "url", 342 | "value": "git@github.com:jquad-group/sample-go-application.git" 343 | }, 344 | { 345 | "name": "url-alternate", 346 | "value": "git@github.com:jquad-group/sample-go-application.git" 347 | }, 348 | { 349 | "name": "revision", 350 | "value": "feature/added-catalog-info" 351 | }, 352 | { 353 | "name": "refspec", 354 | "value": "+refs/tags/*:refs/tags/* +refs/heads/*:refs/heads/*" 355 | }, 356 | { 357 | "name": "deleteExisting", 358 | "value": "true" 359 | }, 360 | { 361 | "name": "sslVerify", 362 | "value": "false" 363 | }, 364 | { 365 | "name": "depth", 366 | "value": "1" 367 | }, 368 | { 369 | "name": "subdirectory", 370 | "value": "" 371 | } 372 | ], 373 | "taskRef": { 374 | "kind": "Task", 375 | "name": "git-clone" 376 | }, 377 | "workspaces": [ 378 | { 379 | "name": "repo", 380 | "workspace": "workspace" 381 | } 382 | ] 383 | }, 384 | { 385 | "name": "github-commit-status-pending", 386 | "params": [ 387 | { 388 | "name": "statusUrl", 389 | "value": "https://api.github.com/repos/jquad-group/sample-go-application/statuses/ab43364d1c192228832068e93c0a1357412e48af" 390 | }, 391 | { 392 | "name": "description", 393 | "value": "The build is currently running." 394 | }, 395 | { 396 | "name": "state", 397 | "value": "pending" 398 | }, 399 | { 400 | "name": "targetUrl", 401 | "value": "https://rancher.jquad.rocks/k8s/clusters/local/api/v1/namespaces/tekton-pipelines/services/http:tekton-dashboard:9097/proxy/#/pipelineruns" 402 | }, 403 | { 404 | "name": "context", 405 | "value": "jquad-group/tekton-ci" 406 | }, 407 | { 408 | "name": "secretName", 409 | "value": "git-clone" 410 | }, 411 | { 412 | "name": "usernameKey", 413 | "value": "username" 414 | }, 415 | { 416 | "name": "passwordKey", 417 | "value": "password" 418 | } 419 | ], 420 | "runAfter": [ 421 | "clone" 422 | ], 423 | "taskRef": { 424 | "kind": "Task", 425 | "name": "github-commit-status" 426 | } 427 | }, 428 | { 429 | "name": "build-and-test-task-go", 430 | "runAfter": [ 431 | "github-commit-status-pending" 432 | ], 433 | "taskRef": { 434 | "kind": "Task", 435 | "name": "build-and-test-task-go" 436 | }, 437 | "workspaces": [ 438 | { 439 | "name": "repo", 440 | "workspace": "workspace" 441 | } 442 | ] 443 | } 444 | ], 445 | "workspaces": [ 446 | { 447 | "name": "workspace" 448 | } 449 | ] 450 | }, 451 | "skippedTasks": [ 452 | { 453 | "name": "github-commit-status-error", 454 | "reason": "When Expressions evaluated to false", 455 | "whenExpressions": [ 456 | { 457 | "input": "Succeeded", 458 | "operator": "in", 459 | "values": [ 460 | "Failed" 461 | ] 462 | } 463 | ] 464 | } 465 | ], 466 | "startTime": "2022-10-25T18:41:36Z", 467 | "taskRuns": { 468 | "feature-added-catalog-info-xdjk9-build-and-test-task-go": { 469 | "pipelineTaskName": "build-and-test-task-go", 470 | "status": { 471 | "completionTime": "2022-10-25T18:42:22Z", 472 | "conditions": [ 473 | { 474 | "lastTransitionTime": "2022-10-25T18:42:22Z", 475 | "message": "All Steps have completed executing", 476 | "reason": "Succeeded", 477 | "status": "True", 478 | "type": "Succeeded" 479 | } 480 | ], 481 | "podName": "feature-added-catalog-info-xdjk9-build-and-test-task-go-pod", 482 | "startTime": "2022-10-25T18:42:07Z", 483 | "steps": [ 484 | { 485 | "container": "step-test", 486 | "imageID": "docker.io/library/golang@sha256:b1ff3263f543b4c458e172f3df429a3e7dfed29d4359ab839a614f903252e1df", 487 | "name": "test", 488 | "terminated": { 489 | "containerID": "containerd://fb2c58d4ac624483829c480e7a1b1532296741645a11fe7b63c93bc2d36c54d0", 490 | "exitCode": 0, 491 | "finishedAt": "2022-10-25T18:42:21Z", 492 | "reason": "Completed", 493 | "startedAt": "2022-10-25T18:42:21Z" 494 | } 495 | }, 496 | { 497 | "container": "step-build", 498 | "imageID": "docker.io/library/golang@sha256:b1ff3263f543b4c458e172f3df429a3e7dfed29d4359ab839a614f903252e1df", 499 | "name": "build", 500 | "terminated": { 501 | "containerID": "containerd://d83b2cdce00d458ac38c428c321aafa9e8f0014a561ef41b7731e28520f7ee00", 502 | "exitCode": 0, 503 | "finishedAt": "2022-10-25T18:42:21Z", 504 | "reason": "Completed", 505 | "startedAt": "2022-10-25T18:42:21Z" 506 | } 507 | } 508 | ], 509 | "taskSpec": { 510 | "steps": [ 511 | { 512 | "image": "golang:1.17.11", 513 | "imagePullPolicy": "IfNotPresent", 514 | "name": "test", 515 | "resources": {}, 516 | "script": "echo \"cd /workspace/repo\"\necho \"make test\"" 517 | }, 518 | { 519 | "image": "golang:1.17.11", 520 | "imagePullPolicy": "IfNotPresent", 521 | "name": "build", 522 | "resources": {}, 523 | "script": "echo \"cd /workspace/repo\"\necho \"make\" " 524 | } 525 | ], 526 | "workspaces": [ 527 | { 528 | "name": "repo" 529 | } 530 | ] 531 | } 532 | } 533 | }, 534 | "feature-added-catalog-info-xdjk9-clone": { 535 | "pipelineTaskName": "clone", 536 | "status": { 537 | "completionTime": "2022-10-25T18:41:58Z", 538 | "conditions": [ 539 | { 540 | "lastTransitionTime": "2022-10-25T18:41:58Z", 541 | "message": "All Steps have completed executing", 542 | "reason": "Succeeded", 543 | "status": "True", 544 | "type": "Succeeded" 545 | } 546 | ], 547 | "podName": "feature-added-catalog-info-xdjk9-clone-pod", 548 | "resourcesResult": [ 549 | { 550 | "key": "url", 551 | "value": "git@github.com:jquad-group/sample-go-application.git" 552 | } 553 | ], 554 | "startTime": "2022-10-25T18:41:36Z", 555 | "steps": [ 556 | { 557 | "container": "step-clone", 558 | "imageID": "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init@sha256:ec618dfa0fc23ae90105135be96bb8c0f9250bfd7682d9d5bab845ab6fd50528", 559 | "name": "clone", 560 | "terminated": { 561 | "containerID": "containerd://8bad7073f6311c42171babfd74fe073ea3275c16308fbfffee2b2c81f25fc7d9", 562 | "exitCode": 0, 563 | "finishedAt": "2022-10-25T18:41:57Z", 564 | "message": "[{\"key\":\"commit\",\"value\":\"ab43364d1c192228832068e93c0a1357412e48af\",\"type\":1},{\"key\":\"lock-pid\",\"value\":\"caa37cd2-f38b-4689-add3-8efeac75715f\\n\",\"type\":1},{\"key\":\"url\",\"value\":\"git@github.com:jquad-group/sample-go-application.git\"}]", 565 | "reason": "Completed", 566 | "startedAt": "2022-10-25T18:41:56Z" 567 | } 568 | } 569 | ], 570 | "taskResults": [ 571 | { 572 | "name": "commit", 573 | "type": "string", 574 | "value": "ab43364d1c192228832068e93c0a1357412e48af" 575 | }, 576 | { 577 | "name": "lock-pid", 578 | "type": "string", 579 | "value": "caa37cd2-f38b-4689-add3-8efeac75715f\n" 580 | } 581 | ], 582 | "taskSpec": { 583 | "params": [ 584 | { 585 | "description": "git https or ssh url to clone", 586 | "name": "url", 587 | "type": "string" 588 | }, 589 | { 590 | "default": "", 591 | "description": "git https or ssh url to clone if not provided, url is used as default", 592 | "name": "url-alternate", 593 | "type": "string" 594 | }, 595 | { 596 | "default": "master", 597 | "description": "git revision to checkout (branch, tag, sha, ref)", 598 | "name": "revision", 599 | "type": "string" 600 | }, 601 | { 602 | "default": "", 603 | "description": "(optional) git refspec to fetch before checking out revision", 604 | "name": "refspec", 605 | "type": "string" 606 | }, 607 | { 608 | "default": "false", 609 | "description": "defines if the resource should initialize and fetch the submodules", 610 | "name": "submodules", 611 | "type": "string" 612 | }, 613 | { 614 | "default": "1", 615 | "description": "performs a shallow clone where only the most recent commit(s) will be fetched", 616 | "name": "depth", 617 | "type": "string" 618 | }, 619 | { 620 | "default": "true", 621 | "description": "defines if http.sslVerify should be set to true or false in the global git config", 622 | "name": "sslVerify", 623 | "type": "string" 624 | }, 625 | { 626 | "default": "", 627 | "description": "subdirectory inside the \"output\" workspace to clone the git repo into", 628 | "name": "subdirectory", 629 | "type": "string" 630 | }, 631 | { 632 | "default": "false", 633 | "description": "clean out the contents of the repo's destination directory (if it already exists) before trying to clone the repo there", 634 | "name": "deleteExisting", 635 | "type": "string" 636 | }, 637 | { 638 | "default": "", 639 | "description": "git HTTP proxy server for non-SSL requests", 640 | "name": "httpProxy", 641 | "type": "string" 642 | }, 643 | { 644 | "default": "", 645 | "description": "git HTTPS proxy server for SSL requests", 646 | "name": "httpsProxy", 647 | "type": "string" 648 | }, 649 | { 650 | "default": "", 651 | "description": "git no proxy - opt out of proxying HTTP/HTTPS requests", 652 | "name": "noProxy", 653 | "type": "string" 654 | }, 655 | { 656 | "default": "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init:v0.37.0", 657 | "description": "The image used where the git-init binary is.", 658 | "name": "gitInitImage", 659 | "type": "string" 660 | } 661 | ], 662 | "results": [ 663 | { 664 | "description": "The precise commit SHA that was fetched by this Task", 665 | "name": "commit", 666 | "type": "string" 667 | }, 668 | { 669 | "description": "The final execution status should be \"Succeeded\", can be replaced in \u003e tekton v20.x", 670 | "name": "status", 671 | "type": "string" 672 | }, 673 | { 674 | "description": "the pid set by the first step", 675 | "name": "lock-pid", 676 | "type": "string" 677 | } 678 | ], 679 | "steps": [ 680 | { 681 | "image": "gcr.io/tekton-releases/github.com/tektoncd/pipeline/cmd/git-init:v0.37.0", 682 | "imagePullPolicy": "Always", 683 | "name": "clone", 684 | "resources": {}, 685 | "script": "# check if given alternate url starts with https\nURL=\"git@github.com:jquad-group/sample-go-application.git\"\nif echo \"git@github.com:jquad-group/sample-go-application.git\" | grep -E \"^[[:blank:]]*http[s]{0,1}://.+$\" ; then\n URL=\"git@github.com:jquad-group/sample-go-application.git\"\nfi\n\nif echo \"$URL\" | grep -v -E \"^[[:blank:]]*http[s]{0,1}://.+$\" ; then\n echo \"Provide a valid https URI, either in url or in url-alternate\"\nfi\n\nCHECKOUT_DIR=\"/workspace/repo/\"\n\n# Locks\nTARGETDIR=\"/workspace/repo\"\nLOCKFOLDER=$TARGETDIR/.lock\nPIDFILE=$LOCKFOLDER/.pid\nLOGFILE=log\ndeleteLock(){\n pid=$(cat /tekton/results/lock-pid)\n content=$(cat $PIDFILE)\n if [ \"$pid\" == \"$content\" ];then\n echo \"deleting lock\"\n rm -rf $LOCKFOLDER;\n fi\n}\n\n# For exit not applicable in this use case\n#trapfunctions\ntrap_exit() {\n echo \"trapping exit signal\" \u003e\u00262\n deleteLock\n}\ntrap_int() {\n echo \"trapping int signal\" \u003e\u00262\n deleteLock\n exit 1\n}\ntrap_term() {\n echo \"trapping term signal\" \u003e\u00262\n deleteLock\n exit 3\n}\n#\n#\ntrap trap_exit EXIT\ntrap trap_int INT\ntrap trap_term TERM\n\naquireLock(){\n mkdir -pv $TARGETDIR\n let i=1\n while ! mkdir $LOCKFOLDER ; do\n echo \"waiting\"\n sleep $i\n let i+=2;\n if $i \u003e= 60 ; then\n # don't wait indefinitely\n exit 4\n fi\n done\n\n PID=caa37cd2-f38b-4689-add3-8efeac75715f\n echo \"aquired Lock\"\n echo $PID \u003e $PIDFILE\n echo $PID \u003e /tekton/results/lock-pid\n echo $PID\n touch $LOCKFOLDER\n}\n\n\ncleandir() {\n # Delete any existing contents of the repo directory if it exists.\n #\n # We don't just \"rm -rf $CHECKOUT_DIR\" because $CHECKOUT_DIR might be \"/\"\n # or the root of a mounted volume.\n if [[ -d \"$CHECKOUT_DIR\" ]] ; then\n # Delete non-hidden files and directories\n rm -rf \"$CHECKOUT_DIR\"/*\n # Delete files and directories starting with . but excluding .. and .lock\n rm -rf $CHECKOUT_DIR/.[!.]*[!lock]*\n # Delete files and directories starting with .. plus any other character\n rm -rf \"$CHECKOUT_DIR\"/..?*\n fi\n}\n\naquireLock\n\nif [[ \"true\" == \"true\" ]] ; then\n cleandir\nfi\ntest -z \"\" || export HTTP_PROXY=\ntest -z \"\" || export HTTPS_PROXY=\ntest -z \"\" || export NO_PROXY=\n/ko-app/git-init \\\n -url \"$URL\" \\\n -revision \"feature/added-catalog-info\" \\\n -refspec \"+refs/tags/*:refs/tags/* +refs/heads/*:refs/heads/*\" \\\n -path \"$CHECKOUT_DIR\" \\\n -sslVerify=\"false\" \\\n -submodules=\"false\" \\\n -depth \"1\"\ncd \"$CHECKOUT_DIR\"\nRESULT_SHA=\"$(git rev-parse HEAD | tr -d '\\n')\"\n\nEXIT_CODE=\"$?\"\n# deleted by trap\n# deleteLock\nif [ \"$EXIT_CODE\" != 0 ]\nthen\n exit $EXIT_CODE\nfi\n# Make sure we don't add a trailing newline to the result!\necho -n \"$RESULT_SHA\" \u003e /tekton/results/commit" 686 | } 687 | ], 688 | "workspaces": [ 689 | { 690 | "description": "The git repo will be cloned onto the volume backing this workspace", 691 | "name": "repo" 692 | } 693 | ] 694 | } 695 | } 696 | }, 697 | "feature-added-catalog-info-xdjk9-github-commit-status-pending": { 698 | "pipelineTaskName": "github-commit-status-pending", 699 | "status": { 700 | "completionTime": "2022-10-25T18:42:06Z", 701 | "conditions": [ 702 | { 703 | "lastTransitionTime": "2022-10-25T18:42:06Z", 704 | "message": "All Steps have completed executing", 705 | "reason": "Succeeded", 706 | "status": "True", 707 | "type": "Succeeded" 708 | } 709 | ], 710 | "podName": "feature-added-catalog-info-eff154312f63d856ff7e731ca728fd3c-pod", 711 | "startTime": "2022-10-25T18:41:58Z", 712 | "steps": [ 713 | { 714 | "container": "step-set-github-commit-status", 715 | "imageID": "docker.io/alpine/curl@sha256:81372de8c566f2d731bde924bed45230018e6d7c21d051c15e283eb8e06dfa2d", 716 | "name": "set-github-commit-status", 717 | "terminated": { 718 | "containerID": "containerd://7a0e6b0c6a032743c2fda5bfb7a0b9e44be36eb07394fb46a66eae78db2f240b", 719 | "exitCode": 0, 720 | "finishedAt": "2022-10-25T18:42:06Z", 721 | "reason": "Completed", 722 | "startedAt": "2022-10-25T18:42:06Z" 723 | } 724 | } 725 | ], 726 | "taskSpec": { 727 | "params": [ 728 | { 729 | "name": "statusUrl", 730 | "type": "string" 731 | }, 732 | { 733 | "default": "success", 734 | "name": "state", 735 | "type": "string" 736 | }, 737 | { 738 | "name": "description", 739 | "type": "string" 740 | }, 741 | { 742 | "default": "https://rancher.jquad.rocks/pipeline", 743 | "name": "targetUrl", 744 | "type": "string" 745 | }, 746 | { 747 | "default": "jquad-group/tekton-ci", 748 | "name": "context", 749 | "type": "string" 750 | }, 751 | { 752 | "name": "secretName", 753 | "type": "string" 754 | }, 755 | { 756 | "default": "username", 757 | "name": "usernameKey", 758 | "type": "string" 759 | }, 760 | { 761 | "default": "password", 762 | "name": "passwordKey", 763 | "type": "string" 764 | } 765 | ], 766 | "steps": [ 767 | { 768 | "env": [ 769 | { 770 | "name": "GITHUB_PASSWORD", 771 | "valueFrom": { 772 | "secretKeyRef": { 773 | "key": "password", 774 | "name": "git-clone" 775 | } 776 | } 777 | }, 778 | { 779 | "name": "GITHUB_USER", 780 | "valueFrom": { 781 | "secretKeyRef": { 782 | "key": "username", 783 | "name": "git-clone" 784 | } 785 | } 786 | } 787 | ], 788 | "image": "alpine/curl:latest", 789 | "imagePullPolicy": "Always", 790 | "name": "set-github-commit-status", 791 | "resources": {}, 792 | "script": "curl \\\n-i \\\n-u \\\n\"${GITHUB_USER}:${GITHUB_PASSWORD}\" \\\n-X \\\nPOST \\\n-H \\\n\"Accept: application/vnd.github.v3+json\" \\\nhttps://api.github.com/repos/jquad-group/sample-go-application/statuses/ab43364d1c192228832068e93c0a1357412e48af \\\n-d \\\n\"{\\\"state\\\":\\\"pending\\\",\\\"target_url\\\":\\\"https://rancher.jquad.rocks/k8s/clusters/local/api/v1/namespaces/tekton-pipelines/services/http:tekton-dashboard:9097/proxy/#/pipelineruns\\\",\\\"description\\\":\\\"The build is currently running.\\\",\\\"context\\\":\\\"jquad-group/tekton-ci\\\"}\"" 793 | } 794 | ] 795 | } 796 | } 797 | }, 798 | "feature-added-catalog-info-xdjk9-github-commit-status-success": { 799 | "pipelineTaskName": "github-commit-status-success", 800 | "status": { 801 | "completionTime": "2022-10-25T18:42:30Z", 802 | "conditions": [ 803 | { 804 | "lastTransitionTime": "2022-10-25T18:42:30Z", 805 | "message": "All Steps have completed executing", 806 | "reason": "Succeeded", 807 | "status": "True", 808 | "type": "Succeeded" 809 | } 810 | ], 811 | "podName": "feature-added-catalog-info-6f82ba3e4ddf3855f05e660aa435643a-pod", 812 | "startTime": "2022-10-25T18:42:22Z", 813 | "steps": [ 814 | { 815 | "container": "step-set-github-commit-status", 816 | "imageID": "docker.io/alpine/curl@sha256:81372de8c566f2d731bde924bed45230018e6d7c21d051c15e283eb8e06dfa2d", 817 | "name": "set-github-commit-status", 818 | "terminated": { 819 | "containerID": "containerd://d4f158d13389fef26d714708f91f226a58868751a7edc496b3a0f3d16f96e69c", 820 | "exitCode": 0, 821 | "finishedAt": "2022-10-25T18:42:29Z", 822 | "reason": "Completed", 823 | "startedAt": "2022-10-25T18:42:29Z" 824 | } 825 | } 826 | ], 827 | "taskSpec": { 828 | "params": [ 829 | { 830 | "name": "statusUrl", 831 | "type": "string" 832 | }, 833 | { 834 | "default": "success", 835 | "name": "state", 836 | "type": "string" 837 | }, 838 | { 839 | "name": "description", 840 | "type": "string" 841 | }, 842 | { 843 | "default": "https://rancher.jquad.rocks/pipeline", 844 | "name": "targetUrl", 845 | "type": "string" 846 | }, 847 | { 848 | "default": "jquad-group/tekton-ci", 849 | "name": "context", 850 | "type": "string" 851 | }, 852 | { 853 | "name": "secretName", 854 | "type": "string" 855 | }, 856 | { 857 | "default": "username", 858 | "name": "usernameKey", 859 | "type": "string" 860 | }, 861 | { 862 | "default": "password", 863 | "name": "passwordKey", 864 | "type": "string" 865 | } 866 | ], 867 | "steps": [ 868 | { 869 | "env": [ 870 | { 871 | "name": "GITHUB_PASSWORD", 872 | "valueFrom": { 873 | "secretKeyRef": { 874 | "key": "password", 875 | "name": "git-clone" 876 | } 877 | } 878 | }, 879 | { 880 | "name": "GITHUB_USER", 881 | "valueFrom": { 882 | "secretKeyRef": { 883 | "key": "username", 884 | "name": "git-clone" 885 | } 886 | } 887 | } 888 | ], 889 | "image": "alpine/curl:latest", 890 | "imagePullPolicy": "Always", 891 | "name": "set-github-commit-status", 892 | "resources": {}, 893 | "script": "curl \\\n-i \\\n-u \\\n\"${GITHUB_USER}:${GITHUB_PASSWORD}\" \\\n-X \\\nPOST \\\n-H \\\n\"Accept: application/vnd.github.v3+json\" \\\nhttps://api.github.com/repos/jquad-group/sample-go-application/statuses/ab43364d1c192228832068e93c0a1357412e48af \\\n-d \\\n\"{\\\"state\\\":\\\"success\\\",\\\"target_url\\\":\\\"https://rancher.jquad.rocks/k8s/clusters/local/api/v1/namespaces/tekton-pipelines/services/http:tekton-dashboard:9097/proxy/#/pipelineruns\\\",\\\"description\\\":\\\"The build was successfull.\\\",\\\"context\\\":\\\"jquad-group/tekton-ci\\\"}\"" 894 | } 895 | ] 896 | } 897 | } 898 | } 899 | } 900 | } 901 | } -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/CollapsibleTable/index.ts: -------------------------------------------------------------------------------- 1 | export { CollapsibleTable } from './CollapsibleTable'; 2 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/CollapsibleTableRow/CollapsibleTableRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StatusError, StatusOK, StatusPending, StatusRunning, StatusWarning } from '@backstage/core-components'; 3 | // eslint-disable-next-line no-restricted-imports 4 | import { KeyboardArrowDown, KeyboardArrowUp } from '@material-ui/icons'; 5 | import { Table, TableBody, TableRow, TableCell, IconButton, Collapse, TableHead } from '@material-ui/core'; 6 | /* ignore lint error for internal dependencies */ 7 | /* eslint-disable */ 8 | import { Condition, PipelineRun } from '../../types'; 9 | import { TaskRunRow } from '../TaskRunRow'; 10 | /* eslint-enable */ 11 | 12 | 13 | function StatusComponent(props: { conditions: [Condition]; }): JSX.Element { 14 | if (props.conditions !== undefined) { 15 | if (props.conditions[0].reason === 'Created') { 16 | return ; 17 | } else 18 | if (props.conditions[0].reason === 'Running') { 19 | return ; 20 | } else 21 | if (props.conditions[0].reason === 'Completed') { 22 | return ; 23 | } else 24 | if (props.conditions[0].reason === 'Succeeded') { 25 | return ; 26 | } else 27 | if (props.conditions[0].reason === 'PipelineRunCancelled') { 28 | return ; 29 | } else 30 | if (props.conditions[0].reason === 'Failed') { 31 | return ; 32 | } else 33 | if (props.conditions[0].reason === 'Error') { 34 | return ; 35 | } 36 | } else { 37 | return ; 38 | } 39 | return ; 40 | 41 | } 42 | 43 | export function CollapsibleTableRow(props: { clusterName: string, pipelineRun: PipelineRun }) { 44 | const { clusterName, pipelineRun } = props; 45 | const [open, setOpen] = React.useState(false); 46 | 47 | if (!pipelineRun || !pipelineRun.status) { 48 | return null; // or handle this case accordingly 49 | } 50 | 51 | if (pipelineRun.status.completionTime === undefined) { 52 | pipelineRun.status.completionTime = ""; 53 | } 54 | 55 | return ( 56 | 57 | 58 | 59 | setOpen(!open)} 63 | > 64 | {open ? : } 65 | 66 | 67 | 68 | {pipelineRun.metadata.name} 69 | 70 | {pipelineRun.metadata.namespace} 71 | { pipelineRun.status.conditions !== undefined && ( 72 | 73 | {pipelineRun.status.conditions[0].reason} 74 | )} 75 | { pipelineRun.status.conditions === undefined && ( 76 | Pending 77 | )} 78 | {pipelineRun.status.startTime} 79 | {pipelineRun.status.completionTime} 80 | Link 81 | 82 | 83 | 84 | 85 | 86 | 87 | Name 88 | Step 89 | Status 90 | Start Time 91 | Completition Time 92 | Log 93 | 94 | 95 | 96 | {pipelineRun.taskRuns !== undefined && 97 | pipelineRun.taskRuns.map((taskRunRow) => ( 98 | 99 | ))} 100 | 101 |
102 |
103 |
104 |
105 | ); 106 | } -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/CollapsibleTableRow/index.ts: -------------------------------------------------------------------------------- 1 | export { CollapsibleTableRow } from './CollapsibleTableRow'; 2 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/StepLog/StepLog.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { 3 | Button, 4 | Dialog, 5 | DialogActions, 6 | DialogContent, 7 | DialogTitle, 8 | IconButton, 9 | Toolbar, 10 | Typography, 11 | } from "@material-ui/core"; 12 | import { makeStyles } from "@material-ui/core/styles"; 13 | import Close from "@material-ui/icons/Close"; 14 | import Fullscreen from "@material-ui/icons/Fullscreen"; 15 | 16 | interface Props { 17 | opened: boolean; 18 | text: string; 19 | } 20 | 21 | const useStyles = makeStyles((theme) => ({ 22 | dialogContent: { 23 | maxHeight: "90vh", 24 | overflowY: "auto", 25 | whiteSpace: "pre-wrap", 26 | fontFamily: "monospace", 27 | }, 28 | toolbar: { 29 | display: "flex", 30 | justifyContent: "space-between", 31 | }, 32 | success: { 33 | color: 'green', 34 | marginLeft: theme.spacing(1), 35 | }, 36 | })); 37 | 38 | export const StepLog: React.FC = ({ opened, text }) => { 39 | const classes = useStyles(); 40 | const [open, setOpen] = useState(opened); 41 | const [copied, setCopied] = useState(false); 42 | const [isFullscreen, setIsFullscreen] = useState(false); 43 | 44 | const toggleFullscreen = () => { 45 | setIsFullscreen(prevState => !prevState); 46 | }; 47 | 48 | const handleCopy = () => { 49 | navigator.clipboard.writeText(text); 50 | setCopied(true); 51 | setTimeout(() => setCopied(false), 2000); 52 | }; 53 | 54 | return ( 55 | setOpen(false)} maxWidth="lg" fullScreen={isFullscreen}> 56 | 57 | 58 | 59 | TaskRun Logs 60 | 61 | 62 | 63 | 64 | setOpen(false)}> 65 | 66 | 67 | 68 | 69 | 70 |
{text}
71 |
72 | { isFullscreen && 73 | 74 | 75 | {copied && Copied!} 76 | 77 | 78 | } 79 | { !isFullscreen && 80 | 81 | 82 | {copied && Copied!} 83 | 84 | 85 | } 86 |
87 | ); 88 | }; 89 | 90 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/StepLog/index.ts: -------------------------------------------------------------------------------- 1 | export { StepLog } from './StepLog'; 2 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/StepRow/StepRow.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { StatusError, StatusOK, StatusPending, StatusRunning, StatusWarning } from '@backstage/core-components'; 3 | // eslint-disable-next-line no-restricted-imports 4 | import { TableRow, TableCell, Button, CircularProgress } from '@material-ui/core'; 5 | import { useApi } from '@backstage/core-plugin-api'; 6 | import { kubernetesApiRef } from '@backstage/plugin-kubernetes'; 7 | 8 | /* ignore lint error for internal dependencies */ 9 | /* eslint-disable */ 10 | import { Step } from '../../types'; 11 | import { StepLog } from '../StepLog'; 12 | 13 | 14 | function StatusComponent(props: { reason: string; }): JSX.Element { 15 | if (props.reason === 'Created') { 16 | return ; 17 | } else 18 | if (props.reason === 'Running') { 19 | return ; 20 | } else 21 | if (props.reason === 'Completed') { 22 | return ; 23 | } else 24 | if (props.reason === 'Succeeded') { 25 | return ; 26 | } else 27 | if (props.reason === 'PipelineRunCancelled') { 28 | return ; 29 | } else 30 | if (props.reason === 'Failed') { 31 | return ; 32 | } 33 | if (props.reason === 'Error') { 34 | return ; 35 | } 36 | return ; 37 | 38 | } 39 | 40 | export function StepRow(props: { clusterName: string, namespace: string, podName: string, step: Step }) { 41 | const { clusterName, namespace, podName, step } = props; 42 | 43 | const [data, setData] = React.useState({data: ""}); 44 | const [isLoading, setIsLoading] = React.useState(false); 45 | 46 | const k8s = useApi(kubernetesApiRef); 47 | 48 | const handleClick = async (step: Step) => { 49 | setIsLoading(true); 50 | const url = `/api/v1/namespaces/${namespace}/pods/${podName}/log?container=step-${step.name}`; 51 | 52 | k8s.proxy({ 53 | clusterName: clusterName, 54 | path: url, 55 | }).then(async (res) => { 56 | setData({...data, data: await res.text()}); 57 | step.log = data.data; 58 | setIsLoading(false); 59 | }); 60 | 61 | }; 62 | 63 | return ( 64 | 65 | 66 | 67 | {step.name} 68 | 69 | {step.terminated !== undefined && ( 70 | <> 71 | {step.terminated.reason} 72 | 73 | {step.terminated.startedAt} 74 | 75 | {step.terminated.finishedAt} 76 | 77 | 78 | {isLoading && ( 79 | 80 | )} 81 | 82 | {!isLoading && data.data !== "" && ( 83 | 84 | )} 85 | 86 | )} 87 | 88 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/StepRow/index.ts: -------------------------------------------------------------------------------- 1 | export { StepRow } from './StepRow'; 2 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/TaskRunRow/TaskRunRow.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | // eslint-disable-next-line no-restricted-imports 3 | import { TableRow, TableCell } from '@material-ui/core'; 4 | /* ignore lint error for internal dependencies */ 5 | /* eslint-disable */ 6 | import { TaskRun } from '../../types'; 7 | import { StepRow } from '../StepRow'; 8 | 9 | export function TaskRunRow(props: { clusterName: string, taskRun: TaskRun }) { 10 | const { clusterName, taskRun } = props; 11 | 12 | return ( 13 | 14 | 15 | {taskRun.status !== undefined && taskRun.status.steps !== undefined && ( 16 | 17 | {taskRun.metadata.name} 18 | 19 | )} 20 | 21 | {taskRun.status !== undefined && taskRun.status.steps !== undefined && 22 | taskRun.status.steps.map((step) => ( 23 | 24 | ))} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/TaskRunRow/index.ts: -------------------------------------------------------------------------------- 1 | export { TaskRunRow } from './TaskRunRow'; 2 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/TektonDashboard/TektoDashboardComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Entity } from '@backstage/catalog-model'; 2 | import { 3 | Content, 4 | ContentHeader, 5 | Page, 6 | Progress, 7 | ResponseErrorPanel, 8 | } from '@backstage/core-components'; 9 | 10 | /* ignore lint error for internal dependencies */ 11 | /* eslint-disable */ 12 | import { Box, FormControl, Grid, InputLabel, MenuItem, Select } from '@material-ui/core'; 13 | import { KubernetesApi, kubernetesApiRef, useKubernetesObjects } from '@backstage/plugin-kubernetes'; 14 | import { PipelineRun, Cluster, TaskRun } from '../../types'; 15 | import { CollapsibleTable } from '../CollapsibleTable'; 16 | import React, { useEffect, useState } from 'react'; 17 | import { useApi } from '@backstage/core-plugin-api'; 18 | 19 | type KubernetesContentProps = { 20 | entity: Entity; 21 | refreshIntervalMs?: number; 22 | children?: React.ReactNode; 23 | }; 24 | 25 | /* 26 | const getTaskRunsForPipelineRun = async (clusterName: string, pipelineRun: PipelineRun, k8s: KubernetesApi): Promise> => { 27 | const namespace = pipelineRun.metadata.namespace; 28 | const pipelineRunName = pipelineRun.metadata.name; 29 | const url = `/apis/tekton.dev/v1/namespaces/${namespace}/taskruns?labelSelector=tekton.dev/pipelineRun=${pipelineRunName}` 30 | 31 | try { 32 | const response = await k8s.proxy({ 33 | clusterName: clusterName, 34 | path: url, 35 | }); 36 | 37 | 38 | if (response && response.status === 200) { 39 | const responseData = await response.json(); 40 | if (responseData && responseData.items) { 41 | const taskRuns: Array = responseData.items; 42 | return taskRuns; 43 | } 44 | } 45 | 46 | return [] 47 | 48 | } catch (error) { 49 | console.error(`Error fetching TaskRuns for PipelineRun ${pipelineRunName}:`, error); 50 | return [] 51 | // throw error; // Rethrow the error to be caught by the calling function 52 | } 53 | }; 54 | */ 55 | 56 | const fetchClusterNames = async (k8s: KubernetesApi): Promise> => { 57 | const clusters: Array = []; 58 | for (const cluster of await k8s.getClusters()) { 59 | let tmpCluster = {} as Cluster; 60 | tmpCluster.name = cluster.name; 61 | clusters.push(tmpCluster); 62 | } 63 | 64 | return clusters; 65 | }; 66 | 67 | const getTaskRunsForCluster = async (clusterName: string, tektonApi: string, labelSelector: string, k8s: KubernetesApi): Promise> => { 68 | 69 | const url = `/apis/tekton.dev/${tektonApi}/taskruns?labelSelector=${labelSelector}&limit=500` 70 | 71 | try { 72 | const response = await k8s.proxy({ 73 | clusterName: clusterName, 74 | path: url, 75 | }); 76 | 77 | if (response && response.status === 200) { 78 | const responseData = await response.json(); 79 | if (responseData && responseData.items) { 80 | const taskRuns: Array = responseData.items; 81 | return taskRuns; 82 | } 83 | } 84 | 85 | return [] 86 | 87 | } catch (error) { 88 | console.error(`Error fetching TaskRuns:`, error); 89 | return [] 90 | } 91 | }; 92 | 93 | const getPipelineRunsForCluster = async (clusterName: string, tektonApi: string, labelSelector: string, k8s: KubernetesApi): Promise> => { 94 | 95 | const url = `/apis/tekton.dev/${tektonApi}/pipelineruns?labelSelector=${labelSelector}&limit=500` 96 | 97 | try { 98 | const response = await k8s.proxy({ 99 | clusterName: clusterName, 100 | path: url, 101 | }); 102 | 103 | if (response && response.status === 200) { 104 | const responseData = await response.json(); 105 | if (responseData && responseData.items) { 106 | const pipelineRuns: Array = responseData.items; 107 | return pipelineRuns; 108 | } 109 | } 110 | 111 | return [] 112 | 113 | } catch (error) { 114 | console.error(`Error fetching PipelineRuns:`, error); 115 | return [] 116 | } 117 | }; 118 | 119 | 120 | export const TektonDashboardComponent = ({ 121 | entity, 122 | refreshIntervalMs, 123 | }: KubernetesContentProps) => { 124 | const { kubernetesObjects, error } = useKubernetesObjects( 125 | entity, 126 | refreshIntervalMs, 127 | ); 128 | 129 | const [loading, setLoading] = useState(true); // State to manage loading state 130 | const [selectedCluster, setSelectedCluster] = useState(null); 131 | const [clusters, setClusters] = useState>([]); 132 | const [fetchingData, setFetchingData] = useState(false); // State to manage fetching data state 133 | const k8s = useApi(kubernetesApiRef); 134 | 135 | useEffect(() => { 136 | const fetchClusters = async () => { 137 | try { 138 | const fetchedClusters = await fetchClusterNames(k8s); 139 | setClusters(fetchedClusters); 140 | setLoading(false); 141 | } catch (error) { 142 | console.error('Error fetching cluster names:', error); 143 | } 144 | }; 145 | 146 | fetchClusters(); 147 | }, []); 148 | 149 | useEffect(() => { 150 | // This useEffect will run whenever kubernetesObjects changes 151 | const loadedCluster = kubernetesObjects?.items.find(obj => obj.cluster.name === selectedCluster); 152 | if (loadedCluster === undefined && selectedCluster) { 153 | setFetchingData(true); 154 | } 155 | if (loadedCluster !== undefined && selectedCluster !== null) { 156 | const fetchTaskRuns = async () => { 157 | // Create a copy of the clusters array 158 | const updatedClusters = [...clusters]; 159 | 160 | // Find the selected cluster in the array 161 | const selectedClusterIndex = updatedClusters.findIndex(cluster => cluster.name === selectedCluster); 162 | 163 | if (loadedCluster.errors.length === 0) { 164 | // create tmpCluster 165 | const tmpCluster = {} as Cluster; 166 | tmpCluster.name = loadedCluster.cluster.name; 167 | 168 | // get all PipelineRuns 169 | let pipelineRunsPromise: Promise; 170 | if (entity?.metadata?.annotations?.["tektonci/api"]) { 171 | const tektonApi = entity?.metadata?.annotations?.["tektonci/api"]; 172 | if (entity?.metadata?.annotations?.["backstage.io/kubernetes-label-selector"]) { 173 | pipelineRunsPromise = getPipelineRunsForCluster(tmpCluster.name, tektonApi, entity.metadata.annotations["backstage.io/kubernetes-label-selector"], k8s); 174 | } else if (entity?.metadata?.annotations?.["backstage.io/kubernetes-namespace"]) { 175 | pipelineRunsPromise = getPipelineRunsForCluster(tmpCluster.name, tektonApi, entity.metadata.annotations["backstage.io/kubernetes-namespace"], k8s); 176 | } else { 177 | pipelineRunsPromise = getPipelineRunsForCluster(tmpCluster.name, tektonApi, "", k8s); 178 | } 179 | } else { 180 | const tektonApi = "v1"; 181 | if (entity?.metadata?.annotations?.["backstage.io/kubernetes-label-selector"]) { 182 | pipelineRunsPromise = getPipelineRunsForCluster(tmpCluster.name, tektonApi, entity.metadata.annotations["backstage.io/kubernetes-label-selector"], k8s); 183 | } else if (entity?.metadata?.annotations?.["backstage.io/kubernetes-namespace"]) { 184 | pipelineRunsPromise = getPipelineRunsForCluster(tmpCluster.name, tektonApi, entity.metadata.annotations["backstage.io/kubernetes-namespace"], k8s); 185 | } else { 186 | pipelineRunsPromise = getPipelineRunsForCluster(tmpCluster.name, tektonApi, "", k8s); 187 | } 188 | } 189 | 190 | 191 | // get all taskruns 192 | let taskRunsPromise: Promise; 193 | if (entity?.metadata?.annotations?.["tektonci/api"]) { 194 | const tektonApi = entity?.metadata?.annotations?.["tektonci/api"]; 195 | if (entity?.metadata?.annotations?.["backstage.io/kubernetes-label-selector"]) { 196 | taskRunsPromise = getTaskRunsForCluster(tmpCluster.name, tektonApi, entity.metadata.annotations["backstage.io/kubernetes-label-selector"], k8s); 197 | } else if (entity?.metadata?.annotations?.["backstage.io/kubernetes-namespace"]) { 198 | taskRunsPromise = getTaskRunsForCluster(tmpCluster.name, tektonApi, entity.metadata.annotations["backstage.io/kubernetes-namespace"], k8s); 199 | } else { 200 | taskRunsPromise = getTaskRunsForCluster(tmpCluster.name, tektonApi, "", k8s); 201 | } 202 | } else { 203 | const tektonApi = "v1"; 204 | if (entity?.metadata?.annotations?.["backstage.io/kubernetes-label-selector"]) { 205 | taskRunsPromise = getTaskRunsForCluster(tmpCluster.name, tektonApi, entity.metadata.annotations["backstage.io/kubernetes-label-selector"], k8s); 206 | } else if (entity?.metadata?.annotations?.["backstage.io/kubernetes-namespace"]) { 207 | taskRunsPromise = getTaskRunsForCluster(tmpCluster.name, tektonApi, entity.metadata.annotations["backstage.io/kubernetes-namespace"], k8s); 208 | } else { 209 | taskRunsPromise = getTaskRunsForCluster(tmpCluster.name, tektonApi, "", k8s); 210 | } 211 | } 212 | 213 | const [allPipelineRuns, allTaskRuns] = await Promise.all([pipelineRunsPromise, taskRunsPromise]); 214 | tmpCluster.pipelineRuns = allPipelineRuns; 215 | 216 | for (const pipelineRun of tmpCluster.pipelineRuns) { 217 | const dashboardAnnotation = "tektonci." + tmpCluster.name + "/dashboard"; 218 | // set dashboardUrl 219 | if (entity.metadata.annotations?.[dashboardAnnotation] !== undefined) { 220 | const dashboardUrl = entity.metadata.annotations[dashboardAnnotation] ?? ""; 221 | const replacedUrl = dashboardUrl 222 | .replace(/\$namespace/g, encodeURIComponent(pipelineRun.metadata.namespace)) 223 | .replace(/\$pipelinerun/g, encodeURIComponent(pipelineRun.metadata.name)); 224 | pipelineRun.pipelineRunDashboardUrl = replacedUrl; 225 | } 226 | // assign the taskruns to the relevant pipelineruns 227 | const taskRunsForPipelineRun: Array = []; 228 | for (const taskRun of allTaskRuns) { 229 | const pipelineRunNameLabel = taskRun.metadata.labels['tekton.dev/pipelineRun']; 230 | if ((String(pipelineRunNameLabel) === pipelineRun.metadata.name) && (taskRun.apiVersion === pipelineRun.apiVersion)) { 231 | taskRunsForPipelineRun.push(taskRun) 232 | } 233 | } 234 | // sort the taskruns associated to a pipelinerun based on start time 235 | taskRunsForPipelineRun.sort((taskRunA, taskRunB) => 236 | (taskRunA?.status?.startTime ?? 0) > (taskRunB?.status?.startTime ?? 0) ? -1 : 1 237 | ); 238 | pipelineRun.taskRuns = taskRunsForPipelineRun; 239 | } 240 | 241 | // sort the pipelineruns 242 | tmpCluster.pipelineRuns.sort((pipelineA, pipelineB) => 243 | (pipelineA?.status?.startTime ?? 0) > (pipelineB?.status?.startTime ?? 0) ? -1 : 1 244 | ); 245 | 246 | updatedClusters[selectedClusterIndex] = { 247 | ...updatedClusters[selectedClusterIndex], 248 | pipelineRuns: tmpCluster.pipelineRuns, 249 | }; 250 | 251 | setClusters(updatedClusters); 252 | setFetchingData(false); 253 | } else if (loadedCluster && loadedCluster.errors.length > 0) { 254 | const tmpCluster = {} as Cluster; 255 | tmpCluster.name = loadedCluster.cluster.name; 256 | tmpCluster.error = loadedCluster.errors[0].errorType; // + ":" + JSON.stringify(kubernetesObject.errors[0], null, 2); 257 | updatedClusters[selectedClusterIndex] = tmpCluster; 258 | setClusters(updatedClusters); 259 | setFetchingData(false); 260 | } else { 261 | updatedClusters[selectedClusterIndex] = { 262 | ...updatedClusters[selectedClusterIndex], 263 | pipelineRuns: [], 264 | }; 265 | setClusters(updatedClusters); 266 | } 267 | }; 268 | fetchTaskRuns() 269 | } 270 | 271 | //}, [kubernetesObjects?.items.find(obj => obj.cluster.name === selectedCluster), selectedCluster]); 272 | }, [kubernetesObjects, selectedCluster]); 273 | 274 | const handleClusterChange = (event: React.ChangeEvent<{ value: unknown }>) => { 275 | if (selectedCluster !== null) { 276 | setFetchingData(true); 277 | } 278 | setSelectedCluster(event.target.value as string); 279 | }; 280 | 281 | return ( 282 | 283 | 284 | {loading ? ( 285 |
286 | 287 |
288 | ) : ( 289 | clusters?.length > 0 && ( 290 | 291 | 292 | 293 | Select Cluster 294 | 306 | 307 | 308 | 309 | 310 | {fetchingData && selectedCluster && ( 311 |
312 | 313 |
314 | ) 315 | } 316 |
317 |
318 | {!fetchingData && selectedCluster && kubernetesObjects?.items !== undefined && ( 319 | 320 | 321 | {clusters 322 | .filter((cluster) => cluster.name === selectedCluster) 323 | .map((cluster) => { 324 | if (cluster.error) { 325 | return ( 326 | 327 | {`Error fetching data for cluster ${cluster.name}: ${cluster.error}`} 328 | 329 | ); 330 | } else if ( 331 | cluster.pipelineRuns !== undefined && 332 | cluster.pipelineRuns !== null && 333 | cluster.pipelineRuns.length > 0 334 | ) { 335 | return ( 336 | 341 | ); 342 | } else if (!fetchingData && (cluster.pipelineRuns === undefined || cluster.pipelineRuns.length === 0)) { 343 | return ( 344 | 345 | No pipeline runs for the selected cluster. 346 | 347 | ); 348 | } else { 349 | return null; 350 | } 351 | })} 352 | 353 | )} 354 |
355 | ) 356 | )} 357 | {error !== undefined && ( 358 | 359 | 360 | ; 361 | 362 | 363 | )} 364 |
365 |
366 | ); 367 | } -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/TektonDashboard/__fixtures__/cluster.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Cluster1", 3 | "pipelineRuns": [ 4 | { 5 | "metadata": { 6 | "labels": { 7 | "testKey": { 8 | "key": "test-key", 9 | "value": "test-value" 10 | } 11 | }, 12 | "name": "feature-added-catalog-info-xdjk9", 13 | "namespace": "sample-go-application-build" 14 | }, 15 | "pipelineRunDashboardUrl": "https://mock.dashboard", 16 | "taskRuns": [ 17 | { 18 | "metadata": { 19 | "name": "taskrun-1", 20 | "namespace": "sample-go-application-build", 21 | "labels": { 22 | "testKey": { 23 | "key": "test-key", 24 | "value": "test-value" 25 | } 26 | } 27 | }, 28 | "status": { 29 | "podName": "taskrun-1-pod", 30 | "startTime": "2022-10-25T18:42:30.000Z", 31 | "completionTime": "2022-10-25T18:47:30.000Z", 32 | "duration": 5, 33 | "durationString": "5m", 34 | "steps": [ 35 | { 36 | "container": "taskrun-container", 37 | "log": "some log", 38 | "name": "taskrun-name", 39 | "terminated": { 40 | "duration": 5, 41 | "durationString": "5m", 42 | "finishedAt": "2022-10-25T18:47:30.000Z", 43 | "reason": "Completed", 44 | "startedAt": "2022-10-25T18:42:30.000Z" 45 | } 46 | } 47 | ], 48 | "conditions": [ 49 | { 50 | "message": "Tasks Completed: 4 (Failed: 0, Cancelled 0), Skipped: 1", 51 | "reason": "Completed", 52 | "status": "True", 53 | "type": "Succeeded" 54 | } 55 | ] 56 | } 57 | }, 58 | { 59 | "metadata": { 60 | "name": "taskrun-2", 61 | "namespace": "sample-go-application-build", 62 | "labels": { 63 | "testKey": { 64 | "key": "test-key", 65 | "value": "test-value" 66 | } 67 | } 68 | }, 69 | "status": { 70 | "podName": "taskrun-2-pod", 71 | "startTime": "2022-10-25T18:42:30.000Z", 72 | "completionTime": "2022-10-25T18:47:30.000Z", 73 | "duration": 5, 74 | "durationString": "5m", 75 | "steps": [ 76 | { 77 | "container": "taskrun-clone", 78 | "log": "clone", 79 | "name": "taskrun-name", 80 | "terminated": { 81 | "duration": 5, 82 | "durationString": "5m", 83 | "finishedAt": "2022-10-25T18:47:30.000Z", 84 | "reason": "Completed", 85 | "startedAt": "2022-10-25T18:42:30.000Z" 86 | } 87 | }, 88 | { 89 | "container": "taskrun-build", 90 | "log": "clone", 91 | "name": "build", 92 | "terminated": { 93 | "duration": 5, 94 | "durationString": "5m", 95 | "finishedAt": "2022-10-25T18:47:30.000Z", 96 | "reason": "Completed", 97 | "startedAt": "2022-10-25T18:42:30.000Z" 98 | } 99 | }, 100 | { 101 | "container": "taskrun-test", 102 | "log": "test", 103 | "name": "test", 104 | "terminated": { 105 | "duration": 5, 106 | "durationString": "5m", 107 | "finishedAt": "2022-10-25T18:47:30.000Z", 108 | "reason": "Completed", 109 | "startedAt": "2022-10-25T18:42:30.000Z" 110 | } 111 | } 112 | ], 113 | "conditions": [ 114 | { 115 | "message": "Tasks Completed: 4 (Failed: 0, Cancelled 0), Skipped: 1", 116 | "reason": "Completed", 117 | "status": "True", 118 | "type": "Succeeded" 119 | } 120 | ] 121 | } 122 | } 123 | ], 124 | "status": { 125 | "startTime": "2022-10-25T18:42:30.000Z", 126 | "completionTime": "2022-10-25T18:47:30.000Z", 127 | "duration": 5, 128 | "durationString": "5m", 129 | "conditions": [ 130 | { 131 | "message": "Tasks Completed: 4 (Failed: 0, Cancelled 0), Skipped: 1", 132 | "reason": "Completed", 133 | "status": "True", 134 | "type": "Succeeded" 135 | } 136 | ] 137 | } 138 | } 139 | ] 140 | } -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/TektonDashboard/__fixtures__/pipelinerun.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "labels": { 4 | "testKey": { 5 | "key": "test-key", 6 | "value": "test-value" 7 | } 8 | }, 9 | "name": "feature-added-catalog-info-xdjk9", 10 | "namespace": "sample-go-application-build" 11 | }, 12 | "pipelineRunDashboardUrl": "https://mock.dashboard", 13 | "taskRuns": [ 14 | { 15 | "metadata": { 16 | "name": "taskrun-1", 17 | "namespace": "sample-go-application-build", 18 | "labels": { 19 | "testKey": { 20 | "key": "test-key", 21 | "value": "test-value" 22 | } 23 | } 24 | }, 25 | "status": { 26 | "podName": "taskrun-1-pod", 27 | "startTime": "2022-10-25T18:42:30.000Z", 28 | "completionTime": "2022-10-25T18:47:30.000Z", 29 | "duration": 5, 30 | "durationString": "5m", 31 | "steps": [ 32 | { 33 | "container": "taskrun-container", 34 | "log": "some log", 35 | "name": "taskrun-name", 36 | "terminated": { 37 | "duration": 5, 38 | "durationString": "5m", 39 | "finishedAt": "2022-10-25T18:47:30.000Z", 40 | "reason": "Completed", 41 | "startedAt": "2022-10-25T18:42:30.000Z" 42 | } 43 | } 44 | ], 45 | "conditions": [ 46 | { 47 | "message": "Tasks Completed: 4 (Failed: 0, Cancelled 0), Skipped: 1", 48 | "reason": "Completed", 49 | "status": "True", 50 | "type": "Succeeded" 51 | } 52 | ] 53 | } 54 | }, 55 | { 56 | "metadata": { 57 | "name": "taskrun-2", 58 | "namespace": "sample-go-application-build", 59 | "labels": { 60 | "testKey": { 61 | "key": "test-key", 62 | "value": "test-value" 63 | } 64 | } 65 | }, 66 | "status": { 67 | "podName": "taskrun-2-pod", 68 | "startTime": "2022-10-25T18:42:30.000Z", 69 | "completionTime": "2022-10-25T18:47:30.000Z", 70 | "duration": 5, 71 | "durationString": "5m", 72 | "steps": [ 73 | { 74 | "container": "taskrun-clone", 75 | "log": "clone", 76 | "name": "taskrun-name", 77 | "terminated": { 78 | "duration": 5, 79 | "durationString": "5m", 80 | "finishedAt": "2022-10-25T18:47:30.000Z", 81 | "reason": "Completed", 82 | "startedAt": "2022-10-25T18:42:30.000Z" 83 | } 84 | }, 85 | { 86 | "container": "taskrun-build", 87 | "log": "clone", 88 | "name": "build", 89 | "terminated": { 90 | "duration": 5, 91 | "durationString": "5m", 92 | "finishedAt": "2022-10-25T18:47:30.000Z", 93 | "reason": "Completed", 94 | "startedAt": "2022-10-25T18:42:30.000Z" 95 | } 96 | }, 97 | { 98 | "container": "taskrun-test", 99 | "log": "test", 100 | "name": "test", 101 | "terminated": { 102 | "duration": 5, 103 | "durationString": "5m", 104 | "finishedAt": "2022-10-25T18:47:30.000Z", 105 | "reason": "Completed", 106 | "startedAt": "2022-10-25T18:42:30.000Z" 107 | } 108 | } 109 | ], 110 | "conditions": [ 111 | { 112 | "message": "Tasks Completed: 4 (Failed: 0, Cancelled 0), Skipped: 1", 113 | "reason": "Completed", 114 | "status": "True", 115 | "type": "Succeeded" 116 | } 117 | ] 118 | } 119 | } 120 | ], 121 | "status": { 122 | "startTime": "2022-10-25T18:42:30.000Z", 123 | "completionTime": "2022-10-25T18:47:30.000Z", 124 | "duration": 5, 125 | "durationString": "5m", 126 | "conditions": [ 127 | { 128 | "message": "Tasks Completed: 4 (Failed: 0, Cancelled 0), Skipped: 1", 129 | "reason": "Completed", 130 | "status": "True", 131 | "type": "Succeeded" 132 | } 133 | ] 134 | } 135 | } -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/components/TektonDashboard/index.ts: -------------------------------------------------------------------------------- 1 | export { TektonDashboardComponent } from './TektoDashboardComponent'; -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/index.ts: -------------------------------------------------------------------------------- 1 | export { tektonPipelinesPluginPlugin, EntityTektonPipelinesContent } from './plugin'; 2 | export { isTektonCiAvailable } from './Router'; -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/plugin.test.ts: -------------------------------------------------------------------------------- 1 | import { tektonPipelinesPluginPlugin } from './plugin'; 2 | 3 | describe('tekton-pipelines-plugin', () => { 4 | it('should export plugin', () => { 5 | expect(tektonPipelinesPluginPlugin).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { createPlugin, createRoutableExtension } from '@backstage/core-plugin-api'; 2 | 3 | import { rootRouteRef } from './routes'; 4 | 5 | export const tektonPipelinesPluginPlugin = createPlugin({ 6 | id: 'tekton-pipelines', 7 | routes: { 8 | root: rootRouteRef, 9 | }, 10 | }); 11 | 12 | export type EntityTektonPipelinesContentProps = { 13 | /** 14 | * Sets the refresh interval in milliseconds. The default value is 10000 (10 seconds) 15 | */ 16 | refreshIntervalMs?: number; 17 | }; 18 | 19 | export const EntityTektonPipelinesContent: ( 20 | props: EntityTektonPipelinesContentProps, 21 | ) => JSX.Element = tektonPipelinesPluginPlugin.provide( 22 | createRoutableExtension({ 23 | name: 'EntityTektonPipelinesContent', 24 | component: () => import('./Router').then(m => m.Router), 25 | mountPoint: rootRouteRef, 26 | }), 27 | ); 28 | 29 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/routes.ts: -------------------------------------------------------------------------------- 1 | import { createRouteRef } from '@backstage/core-plugin-api'; 2 | 3 | export const rootRouteRef = createRouteRef({ 4 | id: 'tekton-pipelines', 5 | }); 6 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; -------------------------------------------------------------------------------- /plugins/tekton-pipelines/src/types/types.ts: -------------------------------------------------------------------------------- 1 | export interface Cluster { 2 | name: string; 3 | pipelineRuns: PipelineRun[]; 4 | error: string; 5 | } 6 | 7 | export interface PipelineRun { 8 | apiVersion: string; 9 | kind: string; 10 | metadata: { 11 | name: string; 12 | namespace: string; 13 | labels: Record; 14 | }; 15 | pipelineRunDashboardUrl: string; 16 | taskRuns: Array; 17 | status: { 18 | childReferences: Array 19 | conditions: [Condition]; 20 | startTime: string; 21 | completionTime: string; 22 | duration: number; 23 | durationString: string; 24 | }; 25 | } 26 | 27 | export interface ChildReferences { 28 | apiVersion: string; 29 | kind: string; 30 | name: string; 31 | pipelineTaskName: string; 32 | } 33 | 34 | export interface TaskRun { 35 | apiVersion: string; 36 | kind: string; 37 | metadata: { 38 | name: string; 39 | namespace: string; 40 | labels: Record; 41 | }; 42 | status: { 43 | conditions: [Condition]; 44 | podName: string; 45 | steps: Array; 46 | startTime: string; 47 | completionTime: string; 48 | duration: number; 49 | durationString: string; 50 | }; 51 | } 52 | 53 | export interface Label { 54 | key: string; 55 | value: string; 56 | } 57 | 58 | export interface Step { 59 | container: string; 60 | name: string; 61 | terminated: Terminated; 62 | log: string; 63 | } 64 | 65 | export interface Terminated { 66 | startedAt: string; 67 | finishedAt: string; 68 | duration: number; 69 | durationString: string; 70 | reason: string; 71 | } 72 | 73 | export interface Condition { 74 | reason: string; 75 | type: string; 76 | status: string; 77 | message: string; 78 | } 79 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@backstage/cli/config/tsconfig.json", 3 | "include": [ 4 | "packages/*/src", 5 | "plugins/*/src", 6 | "plugins/*/dev", 7 | "plugins/*/migrations" 8 | ], 9 | "exclude": ["node_modules"], 10 | "compilerOptions": { 11 | "outDir": "dist-types", 12 | "rootDir": "." 13 | } 14 | } 15 | --------------------------------------------------------------------------------