Compare commits

...

No commits in common. "development" and "build" have entirely different histories.

83 changed files with 9752 additions and 40430 deletions

View file

@ -1,6 +1,10 @@
dist-types
.git
.yarn/cache
.yarn/install-state.gz
node_modules
packages/*/dist
packages/*/src
packages/*/node_modules
plugins/*/dist
plugins/*/node_modules
plugins
*.local.yaml
github-integration.yaml
k8s-config.yaml

View file

@ -1 +0,0 @@
playwright.config.ts

View file

@ -1,51 +0,0 @@
name: ci
on: push
jobs:
build:
runs-on: ubuntu-22.04
steps:
-
name: Repository meta
id: repository
run: |
registry=${{ github.server_url }}
registry=${registry##http*://}
echo "registry=${registry}" >> "$GITHUB_OUTPUT"
echo "registry=${registry}"
repository="$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')"
echo "repository=${repository}" >> "$GITHUB_OUTPUT"
echo "repository=${repository}"
-
name: Docker meta
uses: docker/metadata-action@v5
id: docker
with:
images: ${{ steps.repository.outputs.registry }}/${{ steps.repository.outputs.repository }}
-
name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ steps.repository.outputs.registry }}
username: ${{ secrets.PACKAGES_USER }}
password: ${{ secrets.PACKAGES_TOKEN }}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
buildkitd-flags: '--allow-insecure-entitlement network.host'
driver-opts: network=host
-
name: Build and push
uses: docker/build-push-action@v6
with:
push: true
allow: network.host
network: host
platforms: linux/amd64,linux/arm64
tags: ${{ steps.docker.outputs.tags }}

View file

@ -0,0 +1,22 @@
name: Dependabot auto-merge
on:
pull_request_target:
branches: [ main ]
types: [ opened ]
permissions:
pull-requests: write
contents: write
jobs:
enableAutoMerge:
runs-on: ubuntu-latest
if: github.event.pull_request.user.login == 'dependabot[bot]'
steps:
- name: Enable auto-merge for Dependabot PRs
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

7
.gitignore vendored
View file

@ -27,6 +27,9 @@ node_modules/
# Node version directives
.nvmrc
# NPM config files
.npmrc
# dotenv environment variables file
.env
.env.test
@ -50,5 +53,5 @@ site
# vscode database functionality support files
*.session.sql
# E2E test reports
e2e-test-report/
# JetBrains
.idea

View file

@ -1 +0,0 @@
nodeLinker: node-modules

View file

@ -1,94 +0,0 @@
# Stage 1 - Create yarn install skeleton layer
FROM node:20.18.1 AS packages
WORKDIR /app
COPY package.json yarn.lock ./
COPY packages packages
# Comment this out if you don't have any internal plugins
COPY plugins plugins
RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -exec rm -rf {} \+
# Stage 2 - Install dependencies and build packages
FROM node:20.18.1 AS build
# Required for arm64
RUN apt update -y
RUN apt install -y python3 make gcc build-essential bash
USER node
WORKDIR /app
COPY --from=packages --chown=node:node /app .
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
yarn install --network-timeout 600000
COPY --chown=node:node . .
RUN yarn tsc
RUN yarn --cwd packages/backend build
# If you have not yet migrated to package roles, use the following command instead:
# RUN yarn --cwd packages/backend backstage-cli backend:bundle --build-dependencies
RUN mkdir packages/backend/dist/skeleton packages/backend/dist/bundle \
&& tar xzf packages/backend/dist/skeleton.tar.gz -C packages/backend/dist/skeleton \
&& tar xzf packages/backend/dist/bundle.tar.gz -C packages/backend/dist/bundle
# Stage 3 - Build the actual backend image and install production dependencies
FROM node:20.18.1
# Install isolate-vm dependencies, these are needed by the @backstage/plugin-scaffolder-backend.
# Install packages needed to get utility binaries
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends python3 python3-pip python3-venv g++ build-essential ca-certificates curl
RUN yarn config set python /usr/bin/python3
# Add kubectl for the kube apply plugin.
# Add mkdocs for the TechDocs plugin.
RUN if test "$(uname -m)" = "x86_64"; \
then \
curl -L -o /usr/local/bin/kubectl https://dl.k8s.io/release/v1.29.9/bin/linux/amd64/kubectl; \
fi
RUN if test "$(uname -m)" != "x86_64"; \
then \
curl -L -o /usr/local/bin/kubectl https://dl.k8s.io/release/v1.29.9/bin/linux/arm64/kubectl; \
fi
RUN chmod +x /usr/local/bin/kubectl
ENV VIRTUAL_ENV=/opt/venv
RUN python3 -m venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
RUN pip3 install 'mkdocs-techdocs-core==1.4.2' 'mkdocs-awesome-pages-plugin==2.10.1'
# From here on we use the least-privileged `node` user to run the backend.
USER node
# This should create the app dir as `node`.
# If it is instead created as `root` then the `tar` command below will
# fail: `can't create directory 'packages/': Permission denied`.
# If this occurs, then ensure BuildKit is enabled (`DOCKER_BUILDKIT=1`)
# so the app dir is correctly created as `node`.
WORKDIR /app
# Copy the install dependencies from the build stage and context
COPY --from=build --chown=node:node /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton/ ./
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
yarn install --production --network-timeout 600000
# Copy the built packages from the build stage
COPY --from=build --chown=node:node /app/packages/backend/dist/bundle/ ./
# Copy any other files that we need at runtime
COPY --chown=node:node app-config.yaml ./
# This switches many Node.js dependencies to production mode.
ENV NODE_ENV production
CMD ["node", "packages/backend", "--config", "app-config.yaml"]

202
LICENSE Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

116
README.md
View file

@ -1,116 +1,10 @@
# EDP Backstage
# [Backstage](https://backstage.io)
The EDP bespoke version of backstage.
This is your newly scaffolded Backstage App, Good Luck!
With respect to the CNOE stack (where eDF originates from) it is comparable to https://github.com/cnoe-io/backstage-app
To start the app, run:
At the time writing CNOE-backstage-app is "version": "1.28.4"
## Container Images
Container images are pushed to the Cefor Container Registry and available [here](https://forgejo.edf-bootstrap.cx.fg1.ffm.osc.live/DevFW-CICD/-/packages/container/backstage-edp/).
## Local Development
Use of [**edpbuilder**](https://forgejo.edf-bootstrap.cx.fg1.ffm.osc.live/DevFW/edpbuilder.git) is recommended for local setup.
### Create your local cluster
Once edpbuilder is installed on your computer, create a stack that you are interested in. For example:
> Hint: From here on this is the old CNOE README .... no guarantee that this works as described!
### Update Backstage application config
Once all ArgoCD applications are healthy, you need to update a few fields in the [app-config.yaml](./app-config.yaml) file.
#### Update control plane URL
The control plane port must be updated every time a cluster is created. Run the `kubectl cluster-info` command to get the control plane URL. Once you have your URL, update your `app-config.yaml` file at [this line](https://github.com/cnoe-io/backstage-app/blob/9ee3514e51c1a354b7fe85a90117faf8328bfa0b/app-config.yaml#L122).
For example:
```bash
$ kubectl cluster-info
Kubernetes control plane is running at https://127.0.0.1:36463
CoreDNS is running at https://127.0.0.1:36463/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
```
For this particular example output, the `https://127.0.0.1:36463` above is the URL you need to use in your `app-config.yaml`.
#### Update service account token
Since tokens are generated each time the backstage service account is created, you need to update this value as well. The command to retrieve the service account token is:
`kubectl -n backstage exec -it deploy/backstage -- cat /var/run/secrets/kubernetes.io/serviceaccount/token`
Copy the token value and updated the app-config file at [this line](https://github.com/cnoe-io/backstage-app/blob/main/app-config.yaml#L127).
For example:
```bash
$ kubectl -n backstage exec -it deploy/backstage -- cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIsImtpZCI6IkRxbDRCSnNicjFwekFqdmxwNDc5MHJqeUlFSjhxNHU0LV95OC1s...
```
If you do not want to place the token value in your file, you can use environment variables instead:
1. Set [this line](https://github.com/cnoe-io/backstage-app/blob/main/app-config.yaml#L127) value to be `${BACKSTAGE_SA_TOKEN}`.
2. Then export the token value:
```bash
export BACKSTAGE_SA_TOKEN=$(kubectl -n backstage exec -it deploy/backstage -- cat /var/run/secrets/kubernetes.io/serviceaccount/token)
```
#### Update ArgoCD token
ArgoCD admin passwords are generated on each fresh installation. You need to update the configuration file accordingly. To obtain your password, run: `./idpbuilder get secrets -p argocd`. Then update [this line](https://github.com/cnoe-io/backstage-app/blob/9ee3514e51c1a354b7fe85a90117faf8328bfa0b/app-config.yaml#L136)
For example:
```bash
$ ./idpbuilder get secrets -p argocd
---------------------------
Name: argocd-initial-admin-secret
Namespace: argocd
Data:
password : abc
username : admin
```
#### Update Gitea Credentials
Gitea admin passwords are generated on each fresh installation as well. To obtain your password, run: `./idpbuilder get secrets -p argocd`.
Then update [this line](https://github.com/cnoe-io/backstage-app/blob/9ee3514e51c1a354b7fe85a90117faf8328bfa0b/app-config.yaml#L40) and [this line](https://github.com/cnoe-io/backstage-app/blob/9ee3514e51c1a354b7fe85a90117faf8328bfa0b/app-config.yaml#L44).
For example:
```bash
$ ./idpbuilder get secrets -p gitea
---------------------------
Name: gitea-credential
Namespace: gitea
Data:
password : abc
username : giteaAdmin
````
### Start Backstage processes
Once the `app-config.yaml` file is updated, you are ready to start your backstage instance. For development purposes, using two terminal windows or tabs is recommended. You can also run them through your favorite IDE.
In the first terminal tab, install dependencies and start the backend.
```bash
```sh
yarn install
yarn run start-backend
```
In the first terminal tab, run the frontend.
```bash
yarn run start
yarn dev
```

View file

@ -30,6 +30,6 @@ backend:
catalog:
# Overrides the default list locations from app-config.yaml as these contain example data.
# See https://backstage.io/docs/features/software-catalog/#adding-components-to-the-catalog for more details
# See https://backstage.io/docs/features/software-catalog/software-catalog-overview#adding-components-to-the-catalog for more details
# on how to get entities into the catalog.
locations: []

View file

@ -7,7 +7,7 @@ organization:
backend:
# Used for enabling authentication, secret is shared by all backend plugins
# See https://backstage.io/docs/auth/service-to-service-auth for
# See https://backstage.io/docs/tutorials/backend-to-backend-auth for
# information on the format
# auth:
# keys:
@ -30,25 +30,26 @@ backend:
database:
client: better-sqlite3
connection: ':memory:'
cache:
store: memory
# workingDirectory: /tmp # Use this to configure a working directory for the scaffolder, defaults to the OS temp-dir
integrations:
gitea:
- baseUrl: https://cnoe.localtest.me:8443/gitea
host: cnoe.localtest.me:8443
username: giteaAdmin
password: ${GITEA_PASSWORD}
- baseUrl: https://cnoe.localtest.me/gitea
host: cnoe.localtest.me
username: giteaAdmin
password: ${GITEA_PASSWORD}
integrations: {}
# - host: github.com
# # This is a Personal Access Token or PAT from GitHub. You can find out how to generate this token, and more information
# # about setting up the GitHub integration here: https://backstage.io/docs/getting-started/configuration#setting-up-a-github-integration
# token: ${GITHUB_TOKEN}
### Example for how to add your GitHub Enterprise instance using the API:
# - host: ghe.example.net
# apiBaseUrl: https://ghe.example.net/api/v3
# token: ${GHE_TOKEN}
proxy:
### Example for how to add a proxy endpoint for the frontend.
### A typical reason to do this is to handle HTTPS and CORS for internal services.
# endpoints:
# '/test':
# target: 'https://example.com'
# changeOrigin: true
# '/test':
# target: 'https://example.com'
# changeOrigin: true
# Reference documentation http://backstage.io/docs/features/techdocs/configuration
# Note: After experimenting with basic setup, use CI/CD to generate docs
@ -60,76 +61,25 @@ techdocs:
runIn: 'docker' # Alternatives - 'local'
publisher:
type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives.
auth:
# see https://backstage.io/docs/auth/ to learn about auth providers
environment: local # set this to development to enable SSO
session:
secret: abcdfkjalskdfjkla
providers:
guest: {}
keycloak-oidc:
development:
metadataUrl: https://cnoe.localtest.me:8443/keycloak/realms/cnoe/.well-known/openid-configuration
clientId: backstage
clientSecret: ${KEYCLOAK_CLIENT_SECRET}
prompt: auto
environment: local
providers: {}
scaffolder:
# see https://backstage.io/docs/features/software-templates/configuration for software template options
defaultAuthor:
name: backstage-scaffolder
email: noreply
defaultCommitMessage: "backstage scaffolder"
catalog:
import:
entityFilename: catalog-info.yaml
pullRequestBranchName: backstage-integration
rules:
- allow: [ Component, System, API, Resource, Location, Template ]
locations:
- type: url
target: https://cnoe.localtest.me:8443/gitea/giteaAdmin/idpbuilder-localdev-backstage-templates-entities/src/branch/main/catalog-info.yaml
# # Local example template
# - type: file
# target: ../../examples/template/template.yaml
# rules:
# - allow: [Template]
#
# # Local example organizational data
# - type: file
# target: ../../examples/org.yaml
# rules:
# - allow: [User, Group]
- allow: [Component, System, API, Resource, Location, Template]
locations: []
## Uncomment these lines to add more example data
# - type: url
# target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/all.yaml
## Uncomment these lines to add an example org
# - type: url
# target: https://github.com/backstage/backstage/blob/master/packages/catalog-model/examples/acme-corp.yaml
# rules:
# - allow: [User, Group]
kubernetes:
serviceLocatorMethod:
type: 'multiTenant'
clusterLocatorMethods:
- type: 'config'
clusters:
- url: https://127.0.0.1:33277 # you may need to change this
name: local
authProvider: 'serviceAccount'
skipTLSVerify: true
# replace with your own service account token value. e.g. kubectl -n backstage exec -it deploy/backstage -- cat /var/run/secrets/kubernetes.io/serviceaccount/token
serviceAccountToken: eyJhbG......
argocd:
appLocatorMethods:
- type: 'config'
instances:
- name: local
url: https://cnoe.localtest.me:8443/argocd
username: admin
# replace with your argocd password e.g. kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
password: ${ARGOCD_ADMIN_PASSWORD}
argoWorkflows:
baseUrl: https://cnoe.localtest.me:8443/argo-workflows
clusterLocatorMethods: []

View file

@ -1,3 +1,3 @@
{
"version": "1.36.1"
"version": "1.16.0"
}

View file

@ -1,7 +1,7 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: backstage-idpbuilder
name: backstage
description: An example of a Backstage application.
# Example for optional annotations
# annotations:

View file

@ -1,35 +0,0 @@
#!/bin/bash
SERVICE_ACCOUNT_DIR="/var/run/secrets/kubernetes.io/serviceaccount"
KUBERNETES_SERVICE_SCHEME=$(case $KUBERNETES_SERVICE_PORT in 80|8080|8081) echo "http";; *) echo "https"; esac)
KUBERNETES_SERVER_URL="$KUBERNETES_SERVICE_SCHEME"://"$KUBERNETES_SERVICE_HOST":"$KUBERNETES_SERVICE_PORT"
KUBERNETES_CLUSTER_CA_FILE="$SERVICE_ACCOUNT_DIR"/ca.crt
KUBERNETES_NAMESPACE=$(cat "$SERVICE_ACCOUNT_DIR"/namespace)
KUBERNETES_USER_TOKEN=$(cat "$SERVICE_ACCOUNT_DIR"/token)
KUBERNETES_CONTEXT="inCluster"
rm -rf "$HOME"/.kube
mkdir -p "$HOME"/.kube
cat << EOF > "$HOME"/.kube/config
apiVersion: v1
kind: Config
preferences: {}
current-context: $KUBERNETES_CONTEXT
clusters:
- cluster:
server: $KUBERNETES_SERVER_URL
certificate-authority: $KUBERNETES_CLUSTER_CA_FILE
name: inCluster
users:
- name: podServiceAccount
user:
token: $KUBERNETES_USER_TOKEN
contexts:
- context:
cluster: inCluster
user: podServiceAccount
namespace: $KUBERNETES_NAMESPACE
name: $KUBERNETES_CONTEXT
EOF
cnoe-cli "$@"

View file

@ -1,17 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: game-demo
data:
# property-like keys; each key maps to a simple value
player_initial_lives: "3"
ui_properties_file_name: "user-interface.properties"
# file-like keys
game.properties: |
enemy.types=aliens,monsters
player.maximum-lives=5
user-interface.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true

View file

@ -1,41 +0,0 @@
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: deploy-resources-object
title: Deploy Resources using object
description: Deploy Resource to Kubernetes
spec:
owner: guest
type: service
# these are the steps which are rendered in the frontend with the form input
parameters: []
steps:
- id: template
name: Generating component
action: fetch:template
input:
url: ./skeleton
- id: apply
name: apply-manifest
action: cnoe:kubernetes:apply
input:
namespaced: true
manifestObject:
apiVersion: v1
kind: ConfigMap
metadata:
name: game-demo
data:
# property-like keys; each key maps to a simple value
player_initial_lives: "3"
ui_properties_file_name: "user-interface.properties"
# file-like keys
game.properties: |
enemy.types=aliens,monsters
player.maximum-lives=5
user-interface.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true
clusterName: local

View file

@ -1,41 +0,0 @@
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: deploy-resources-string
title: Deploy Resources using literal string
description: Deploy Resource to Kubernetes
spec:
owner: guest
type: service
# these are the steps which are rendered in the frontend with the form input
parameters: []
steps:
- id: template
name: Generating component
action: fetch:template
input:
url: ./skeleton
- id: apply
name: apply-manifest
action: cnoe:kubernetes:apply
input:
namespaced: true
manifestString: |
apiVersion: v1
kind: ConfigMap
metadata:
name: game-demo
data:
# property-like keys; each key maps to a simple value
player_initial_lives: "3"
ui_properties_file_name: "user-interface.properties"
# file-like keys
game.properties: |
enemy.types=aliens,monsters
player.maximum-lives=5
user-interface.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true
clusterName: local

View file

@ -1,30 +0,0 @@
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: deploy-resources
title: Deploy Resources
description: Deploy Resource to Kubernetes
spec:
owner: guest
type: service
# these are the steps which are rendered in the frontend with the form input
parameters:
- title: file name
properties:
path:
type: string
description: file name
default: cm.yaml
steps:
- id: template
name: Generating component
action: fetch:template
input:
url: ./skeleton
- id: apply
name: apply-manifest
action: cnoe:kubernetes:apply
input:
namespaced: true
manifestPath: cm.yaml
clusterName: local

9
github-integration.yaml Normal file
View file

@ -0,0 +1,9 @@
appId: 123456
webhookUrl: https://somehwere
clientId: some.id
clientSecret: ""
webhookSecret: ""
privateKey: |
-----BEGIN RSA PRIVATE KEY-----
-----END RSA PRIVATE KEY-----

16
k8s-config.yaml Normal file
View file

@ -0,0 +1,16 @@
type: 'config'
clusters:
- url: https://3CEBA3CA7870A3E5BFE2CF3FA173EE56.gr7.us-west-2.eks.amazonaws.com:443
name: canoe-packaging
authProvider: 'serviceAccount'
skipTLSVerify: false
skipMetricsLookup: true
serviceAccountToken: ""
# dashboardUrl: http://127.0.0.1:64713 # url copied from running the command: minikube service kubernetes-dashboard -n kubernetes-dashboard
# dashboardApp: standard
caData: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVhZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJek1ERXlOVEl3TkRBMU5Wb1hEVE16TURFeU1qSXdOREExTlZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTlJvCnU5dkl6cjZmVEk4RThyR0Q2RHNoLzhyK0lkWmFHZGxsUytKbDN0Q2JteTVYUU15NnpOMU5acG1zRHpDTC9nUlIKS0s5WTVhUmRUWjFLdklkekRMQXdMeXpqODk5clJtYjB2aXUzR0ZQdDcxSWFYMEp1VmQwaTBrQit5Y01jSFo2QgpjOGhmMUErM1I2VVpCZDZsaUx0dG5pUjZwb29oYXdobG5DSEN4L1oyd014YWEvU21SUWxDMjhhTEhLZC9ZU0s2CndXS1VOQmVTMmpGZGc5bVVkcnJDREx5MkxqUTNUcUtPVW9PNEQ3bm9rVTh1NUFtejhldWFxdzR4U25ZMExucmsKWVk1MmhvOW5qRnZwOE5WQnE1VjRPUFVXaEhvQXE4TnZjZlVITkNSdWZkN09FZG85Y2t1Q1B3VzFiZWxNOW9oeApURFAvWFlsS09INFVQTDFHeUJFQ0F3RUFBYU5aTUZjd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZOeUgrRTZxb2VMTlVEVkl4ZXpTSjk3STRoZytNQlVHQTFVZEVRUU8KTUF5Q0NtdDFZbVZ5Ym1WMFpYTXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBRDVoeStNaDBRdHJ6dG5vV0tFRgpTaFFsanE1cjJGYUZablAyYU9OWS9uaHNxdThjSmZkbWFyQUtsR1JkRTBocnVoaGprdE55ckdmcEZ5d1ErR0hhClR4d0N6NW9uUEhYaTRNZnBadEpaNzZYSERtT3BFR2diSTFhL0VCQUV2YkxHSVRWT3NTMmQ2MTFKTTF0bkJKRFgKNERFaVc5aXJ1Nm1wR2NaQ1JWYlhUT005cHV1V0NTQ1pPNktKZ29NZlVMbnpHT0diN0ludmtoajBJZThQQ0JGWQpWUmFvRm5NNE5HMUdHMnpuckcrNjFucFlBbGpGcjhQN2J4WmRsWWpPcjFGbFhydU1UeEdEZEpNYkNTcFViRmRUCkxOOVUxYlFNS3JBN3NsZEJCcTc0ZHlUZkNKZDFQaGdMSzZZbVZGdFo3Vmk4eFkwbjlpa2svZEpDWjM5aTFWR2wKK3NzPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
# caFile: '' # local path to CA file
customResources:
- group: 'argoproj.io'
apiVersion: 'v1alpha1'
plural: 'applications'

View file

@ -1,6 +1,6 @@
{
"packages": ["packages/*", "plugins/*"],
"npmClient": "yarn",
"version": "0.1.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
"useWorkspaces": true,
"version": "0.1.0"
}

View file

@ -3,7 +3,7 @@
"version": "1.0.0",
"private": true,
"engines": {
"node": "18 || 20"
"node": "16 || 18"
},
"scripts": {
"dev": "concurrently \"yarn start\" \"yarn start-backend\"",
@ -17,8 +17,6 @@
"clean": "backstage-cli repo clean",
"test": "backstage-cli repo test",
"test:all": "backstage-cli repo test --coverage",
"test:e2e": "playwright test",
"fix": "backstage-cli repo fix",
"lint": "backstage-cli repo lint --since origin/main",
"lint:all": "backstage-cli repo lint",
"prettier:check": "prettier --check .",
@ -31,19 +29,17 @@
]
},
"devDependencies": {
"@backstage/cli": "^0.30.0",
"@backstage/e2e-test-utils": "^0.1.1",
"@playwright/test": "^1.32.3",
"@backstage/cli": "^0.22.9",
"@spotify/prettier-config": "^12.0.0",
"concurrently": "^8.0.0",
"lerna": "^7.3.0",
"concurrently": "^6.0.0",
"lerna": "^4.0.0",
"node-gyp": "^9.0.0",
"prettier": "^2.3.2",
"typescript": "~5.2.0"
"typescript": "~4.6.4"
},
"resolutions": {
"@types/react": "^18",
"@types/react-dom": "^18"
"@types/react": "^17",
"@types/react-dom": "^17"
},
"prettier": "@spotify/prettier-config",
"lint-staged": {

View file

@ -1 +0,0 @@
public

View file

@ -0,0 +1,6 @@
{
"baseUrl": "http://localhost:3001",
"fixturesFolder": false,
"pluginsFile": false,
"retries": 3
}

View file

@ -0,0 +1,12 @@
{
"plugins": ["cypress"],
"extends": ["plugin:cypress/recommended"],
"rules": {
"jest/expect-expect": [
"error",
{
"assertFunctionNames": ["expect", "cy.contains"]
}
]
}
}

View file

@ -0,0 +1,6 @@
describe('App', () => {
it('should render the catalog', () => {
cy.visit('/');
cy.contains('My Company Catalog');
});
});

View file

@ -1,23 +0,0 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { test, expect } from '@playwright/test';
test('App should render the welcome page', async ({ page }) => {
await page.goto('/');
await expect(page.getByText('My Company Catalog')).toBeVisible();
});

View file

@ -11,55 +11,69 @@
"build": "backstage-cli package build",
"clean": "backstage-cli package clean",
"test": "backstage-cli package test",
"lint": "backstage-cli package lint"
"lint": "backstage-cli package lint",
"test:e2e": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:dev",
"test:e2e:ci": "cross-env PORT=3001 start-server-and-test start http://localhost:3001 cy:run",
"cy:dev": "cypress open",
"cy:run": "cypress run --browser chrome"
},
"dependencies": {
"@backstage-community/plugin-github-actions": "^0.6.16",
"@backstage-community/plugin-tech-radar": "^0.7.4",
"@backstage/app-defaults": "^1.5.17",
"@backstage/catalog-model": "^1.7.3",
"@backstage/cli": "^0.30.0",
"@backstage/core-app-api": "^1.15.5",
"@backstage/core-components": "^0.16.4",
"@backstage/core-plugin-api": "^1.10.4",
"@backstage/integration-react": "^1.2.4",
"@backstage/plugin-api-docs": "^0.12.4",
"@backstage/plugin-catalog": "^1.27.0",
"@backstage/plugin-catalog-common": "^1.1.3",
"@backstage/plugin-catalog-graph": "^0.4.16",
"@backstage/plugin-catalog-import": "^0.12.10",
"@backstage/plugin-catalog-react": "^1.15.2",
"@backstage/plugin-home": "^0.8.5",
"@backstage/plugin-kubernetes": "^0.12.4",
"@backstage/plugin-org": "^0.6.36",
"@backstage/plugin-permission-react": "^0.4.31",
"@backstage/plugin-scaffolder": "^1.28.0",
"@backstage/plugin-search": "^1.4.23",
"@backstage/plugin-search-react": "^1.8.6",
"@backstage/plugin-techdocs": "^1.12.3",
"@backstage/plugin-techdocs-module-addons-contrib": "^1.1.21",
"@backstage/plugin-techdocs-react": "^1.2.14",
"@backstage/plugin-user-settings": "^0.8.19",
"@backstage/theme": "^0.6.4",
"@backstage/app-defaults": "^1.4.1",
"@backstage/catalog-model": "^1.4.1",
"@backstage/cli": "^0.22.9",
"@backstage/core-app-api": "^1.9.0",
"@backstage/core-components": "^0.13.3",
"@backstage/core-plugin-api": "^1.5.3",
"@backstage/integration-react": "^1.1.15",
"@backstage/plugin-api-docs": "^0.9.6",
"@backstage/plugin-catalog": "^1.12.0",
"@backstage/plugin-catalog-common": "^1.0.15",
"@backstage/plugin-catalog-graph": "^0.2.32",
"@backstage/plugin-catalog-import": "^0.9.10",
"@backstage/plugin-catalog-react": "^1.8.0",
"@backstage/plugin-github-actions": "^0.6.1",
"@backstage/plugin-kubernetes": "^0.9.3",
"@backstage/plugin-org": "^0.6.10",
"@backstage/plugin-permission-react": "^0.4.14",
"@backstage/plugin-scaffolder": "^1.14.1",
"@backstage/plugin-scaffolder-react": "^1.5.1",
"@backstage/plugin-search": "^1.3.3",
"@backstage/plugin-search-react": "^1.6.3",
"@backstage/plugin-tech-radar": "^0.6.6",
"@backstage/plugin-techdocs": "^1.6.5",
"@backstage/plugin-techdocs-module-addons-contrib": "^1.0.15",
"@backstage/plugin-techdocs-react": "^1.1.8",
"@backstage/plugin-user-settings": "^0.7.5",
"@backstage/theme": "^0.4.1",
"@cnoe-io/plugin-apache-spark": "0.1.2",
"@cnoe-io/plugin-argo-workflows": "0.1.3",
"@cnoe-io/plugin-scaffolder-actions-frontend": "0.1.1",
"@internal/cnoe-ui-plugin": "^0.1.0",
"@internal/plugin-workflows": "^0.1.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@roadiehq/backstage-plugin-argo-cd": "^2.5.1",
"@rjsf/core": "^5.8.1",
"@rjsf/utils": "^5.8.1",
"@roadiehq/backstage-plugin-argo-cd": "^2.3.4",
"@types/fs-extra": "^11.0.1",
"fs-extra": "^11.1.1",
"history": "^5.0.0",
"react": "^18.0.2",
"react-dom": "^18.0.2",
"react-router": "^6.3.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^6.3.0",
"react-use": "^17.2.4"
},
"devDependencies": {
"@backstage/test-utils": "^1.7.5",
"@playwright/test": "^1.32.3",
"@testing-library/dom": "^9.0.0",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^14.0.0",
"@backstage/test-utils": "^1.4.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"@types/node": "^16.11.26",
"@types/react-dom": "*",
"cross-env": "^7.0.0"
"cross-env": "^7.0.0",
"cypress": "^9.7.0",
"eslint-plugin-cypress": "^2.10.3",
"start-server-and-test": "^1.10.11"
},
"browserslist": {
"production": [

View file

@ -8,6 +8,7 @@
name="description"
content="Backstage is an open platform for building developer portals"
/>
<link rel="apple-touch-icon" href="<%= publicPath %>/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/

View file

@ -1,5 +1,5 @@
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { renderWithEffects } from '@backstage/test-utils';
import App from './App';
describe('App', () => {
@ -20,10 +20,7 @@ describe('App', () => {
] as any,
};
const rendered = render(<App />);
await waitFor(() => {
expect(rendered.baseElement).toBeInTheDocument();
});
const rendered = await renderWithEffects(<App />);
expect(rendered.baseElement).toBeInTheDocument();
});
});

View file

@ -11,9 +11,10 @@ import {
catalogImportPlugin,
} from '@backstage/plugin-catalog-import';
import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react';
import { orgPlugin } from '@backstage/plugin-org';
import { SearchPage } from '@backstage/plugin-search';
import { TechRadarPage } from '@backstage-community/plugin-tech-radar';
import { TechRadarPage } from '@backstage/plugin-tech-radar';
import {
TechDocsIndexPage,
techdocsPlugin,
@ -22,18 +23,32 @@ import {
import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
import { UserSettingsPage } from '@backstage/plugin-user-settings';
import {apis, keycloakOIDCAuthApiRef} from './apis';
import { apis, keycloakOIDCAuthApiRef } from './apis';
import { entityPage } from './components/catalog/EntityPage';
import { searchPage } from './components/search/SearchPage';
import { Root } from './components/Root';
import {AlertDisplay, OAuthRequestDialog, SignInPage} from '@backstage/core-components';
import {
AlertDisplay,
OAuthRequestDialog,
SignInPage,
} from '@backstage/core-components';
import { createApp } from '@backstage/app-defaults';
import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
import { CatalogGraphPage } from '@backstage/plugin-catalog-graph';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/alpha';
import {configApiRef, useApi} from "@backstage/core-plugin-api";
import { KubernetesClusterPickerExtension } from '@cnoe-io/plugin-scaffolder-actions-frontend';
import { ThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import LightIcon from '@material-ui/icons/WbSunny';
import {
CNOEHomepage,
cnoeLightTheme,
cnoeDarkTheme,
} from '@internal/cnoe-ui-plugin';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
const app = createApp({
apis,
@ -46,6 +61,7 @@ const app = createApp({
return (
<SignInPage
{...props}
auto
provider={{
id: 'keycloak-oidc',
title: 'Keycloak',
@ -60,24 +76,47 @@ const app = createApp({
bind(catalogPlugin.externalRoutes, {
createComponent: scaffolderPlugin.routes.root,
viewTechDoc: techdocsPlugin.routes.docRoot,
createFromTemplate: scaffolderPlugin.routes.selectedTemplate,
});
bind(apiDocsPlugin.externalRoutes, {
registerApi: catalogImportPlugin.routes.importPage,
});
bind(scaffolderPlugin.externalRoutes, {
registerComponent: catalogImportPlugin.routes.importPage,
viewTechDoc: techdocsPlugin.routes.docRoot,
});
bind(orgPlugin.externalRoutes, {
catalogIndex: catalogPlugin.routes.catalogIndex,
});
}
},
themes: [
{
id: 'cnoe-light-theme',
title: 'Light Theme',
variant: 'light',
icon: <LightIcon />,
Provider: ({ children }) => (
<ThemeProvider theme={cnoeLightTheme}>
<CssBaseline>{children}</CssBaseline>
</ThemeProvider>
),
},
{
id: 'cnoe-dark-theme',
title: 'Dark Theme',
variant: 'dark',
icon: <LightIcon />,
Provider: ({ children }) => (
<ThemeProvider theme={cnoeDarkTheme}>
<CssBaseline>{children}</CssBaseline>
</ThemeProvider>
),
},
],
});
const routes = (
<FlatRoutes>
<Route path="/" element={<Navigate to="home" />} />
<Route path="/home" element={<CNOEHomepage />} />
<Route path="/catalog" element={<CatalogIndexPage />} />
<Route
path="/catalog/:namespace/:kind/:name"
@ -94,7 +133,11 @@ const routes = (
<ReportIssue />
</TechDocsAddons>
</Route>
<Route path="/create" element={<ScaffolderPage />} />
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderFieldExtensions>
<KubernetesClusterPickerExtension />
</ScaffolderFieldExtensions>
</Route>
<Route path="/api-docs" element={<ApiExplorerPage />} />
<Route
path="/tech-radar"

View file

@ -4,16 +4,24 @@ import {
ScmAuth,
} from '@backstage/integration-react';
import {
AnyApiFactory, ApiRef, BackstageIdentityApi,
AnyApiFactory,
ApiRef,
BackstageIdentityApi,
configApiRef,
createApiFactory, createApiRef, discoveryApiRef, oauthRequestApiRef, OpenIdConnectApi, ProfileInfoApi, SessionApi,
createApiFactory,
createApiRef,
discoveryApiRef,
oauthRequestApiRef,
OpenIdConnectApi,
ProfileInfoApi,
SessionApi,
} from '@backstage/core-plugin-api';
import {OAuth2} from "@backstage/core-app-api";
import { OAuth2 } from '@backstage/core-app-api';
export const keycloakOIDCAuthApiRef: ApiRef<
OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
> = createApiRef({
id: 'auth.keycloak-oidc',
id: 'auth.keycloak-oidc-provider',
});
export const apis: AnyApiFactory[] = [
createApiFactory({

View file

@ -1,10 +1,13 @@
import React, { PropsWithChildren } from 'react';
import { makeStyles } from '@material-ui/core';
import HomeIcon from '@material-ui/icons/Home';
import CategoryIcon from '@material-ui/icons/Category';
import ExtensionIcon from '@material-ui/icons/Extension';
import MapIcon from '@material-ui/icons/MyLocation';
import LibraryBooks from '@material-ui/icons/LibraryBooks';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import {LogoFull, LogoIcon} from '@internal/cnoe-ui-plugin';
import {
Settings as SidebarSettings,
UserSettingsSignInAvatar,
@ -19,6 +22,7 @@ import {
SidebarPage,
SidebarScrollWrapper,
SidebarSpace,
useSidebarOpenState,
Link,
} from '@backstage/core-components';
import MenuIcon from '@material-ui/icons/Menu';
@ -41,10 +45,12 @@ const useSidebarLogoStyles = makeStyles({
const SidebarLogo = () => {
const classes = useSidebarLogoStyles();
const { isOpen } = useSidebarOpenState();
return (
<div className={classes.root}>
<Link to="/" underline="none" className={classes.link} aria-label="Home">
{isOpen ? <LogoFull /> : <LogoIcon />}
</Link>
</div>
);
@ -60,7 +66,8 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarDivider />
<SidebarGroup label="Menu" icon={<MenuIcon />}>
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
<SidebarItem icon={HomeIcon} to="home" text="Home" />
<SidebarItem icon={CategoryIcon} to="catalog" text="Catalog" />
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
<SidebarItem icon={LibraryBooks} to="docs" text="Docs" />
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />

View file

@ -10,8 +10,11 @@ import {
} from '@backstage/plugin-api-docs';
import {
EntityAboutCard,
EntityDependsOnComponentsCard,
EntityDependsOnResourcesCard,
EntityHasComponentsCard,
EntityHasResourcesCard,
EntityHasSubcomponentsCard,
EntityHasSystemsCard,
EntityLayout,
EntityLinksCard,
@ -22,9 +25,11 @@ import {
isKind,
hasCatalogProcessingErrors,
isOrphan,
hasRelationWarnings,
EntityRelationWarning,
} from '@backstage/plugin-catalog';
import {
isGithubActionsAvailable,
EntityGithubActionsContent,
} from '@backstage/plugin-github-actions';
import {
EntityUserProfileCard,
EntityGroupProfileCard,
@ -51,11 +56,17 @@ import {
import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
import { EntityKubernetesContent, isKubernetesAvailable } from '@backstage/plugin-kubernetes';
import { EntityKubernetesContent } from '@backstage/plugin-kubernetes';
import {
EntityArgoCDOverviewCard,
isArgocdAvailable
EntityArgoWorkflowsOverviewCard,
EntityArgoWorkflowsTemplateOverviewCard,
isArgoWorkflowsAvailable,
} from '@cnoe-io/plugin-argo-workflows';
import { ApacheSparkPage } from '@cnoe-io/plugin-apache-spark';
import {
EntityArgoCDHistoryCard,
isArgocdAvailable,
} from '@roadiehq/backstage-plugin-argo-cd';
const techdocsContent = (
@ -67,7 +78,13 @@ const techdocsContent = (
);
const cicdContent = (
// This is an example of how you can implement your company's logic in entity page.
// You can for example enforce that all components of type 'service' should use GitHubActions
<EntitySwitch>
<EntitySwitch.Case if={isGithubActionsAvailable}>
<EntityGithubActionsContent />
</EntitySwitch.Case>
<EntitySwitch.Case>
<EmptyState
title="No CI/CD available for this entity"
@ -97,14 +114,6 @@ const entityWarningContent = (
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasRelationWarnings}>
<Grid item xs={12}>
<EntityRelationWarning />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<EntitySwitch>
<EntitySwitch.Case if={hasCatalogProcessingErrors}>
<Grid item xs={12}>
@ -122,18 +131,30 @@ const overviewContent = (
<EntityAboutCard variant="gridItem" />
</Grid>
<EntitySwitch>
<EntitySwitch.Case if={e => Boolean(isArgocdAvailable(e))}>
<EntitySwitch.Case if={e => isArgoWorkflowsAvailable(e)}>
<Grid item md={6}>
<EntityArgoCDOverviewCard />
<EntityArgoWorkflowsOverviewCard />
</Grid>
<Grid item md={6}>
<EntityArgoWorkflowsTemplateOverviewCard />
</Grid>
</EntitySwitch.Case>
<EntitySwitch.Case if={e => Boolean(isArgocdAvailable(e))}>
<Grid item sm={6}>
<EntityArgoCDHistoryCard />
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
<Grid item md={6} xs={12}>
<EntityCatalogGraphCard variant="gridItem" height={400} />
</Grid>
<Grid item md={4} xs={12}>
<EntityLinksCard />
</Grid>
<Grid item md={8} xs={12}>
<EntityHasSubcomponentsCard variant="gridItem" />
</Grid>
</Grid>
);
@ -147,10 +168,6 @@ const serviceEntityPage = (
{cicdContent}
</EntityLayout.Route>
<EntityLayout.Route path="/kubernetes" title="Kubernetes" if={e => isKubernetesAvailable(e)}>
<EntityKubernetesContent refreshIntervalMs={30000} />
</EntityLayout.Route>
<EntityLayout.Route path="/api" title="API">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
@ -162,6 +179,17 @@ const serviceEntityPage = (
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/dependencies" title="Dependencies">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityDependsOnComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityDependsOnResourcesCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
{techdocsContent}
</EntityLayout.Route>
@ -178,12 +206,36 @@ const websiteEntityPage = (
{cicdContent}
</EntityLayout.Route>
<EntityLayout.Route path="/dependencies" title="Dependencies">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityDependsOnComponentsCard variant="gridItem" />
</Grid>
<Grid item md={6}>
<EntityDependsOnResourcesCard variant="gridItem" />
</Grid>
</Grid>
</EntityLayout.Route>
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent refreshIntervalMs={30000} />
</EntityLayout.Route>
<EntityLayout.Route path="/docs" title="Docs">
{techdocsContent}
</EntityLayout.Route>
</EntityLayout>
);
const jobEntityPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
<EntityLayout.Route path="/apache-spark" title="Apache Spark">
<ApacheSparkPage />
</EntityLayout.Route>
</EntityLayout>
);
/**
* NOTE: This page is designed to work on small screens such as mobile devices.
* 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`,
@ -212,6 +264,9 @@ const componentPage = (
<EntitySwitch.Case if={isComponentType('website')}>
{websiteEntityPage}
</EntitySwitch.Case>
<EntitySwitch.Case if={isComponentType('job')}>
{jobEntityPage}
</EntitySwitch.Case>
<EntitySwitch.Case>{defaultEntityPage}</EntitySwitch.Case>
</EntitySwitch>
@ -228,6 +283,9 @@ const apiPage = (
<Grid item md={6} xs={12}>
<EntityCatalogGraphCard variant="gridItem" height={400} />
</Grid>
<Grid item md={4} xs={12}>
<EntityLinksCard />
</Grid>
<Grid container item md={12}>
<Grid item md={6}>
<EntityProvidingComponentsCard />
@ -276,12 +334,9 @@ const groupPage = (
<Grid item xs={12} md={6}>
<EntityOwnershipCard variant="gridItem" />
</Grid>
<Grid item xs={12} md={6}>
<Grid item xs={12}>
<EntityMembersListCard />
</Grid>
<Grid item xs={12} md={6}>
<EntityLinksCard />
</Grid>
</Grid>
</EntityLayout.Route>
</EntityLayout>
@ -331,6 +386,9 @@ const systemPage = (
unidirectional={false}
/>
</EntityLayout.Route>
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent refreshIntervalMs={30000} />
</EntityLayout.Route>
</EntityLayout>
);

View file

@ -1,6 +1,6 @@
import '@backstage/cli/asset-types';
import React from 'react';
import ReactDOM from 'react-dom/client';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
ReactDOM.render(<App />, document.getElementById('root'));

View file

@ -9,21 +9,18 @@
#
# Once the commands have been run, you can build the image using `yarn build-image`
FROM node:18-bookworm-slim
# Install isolate-vm dependencies, these are needed by the @backstage/plugin-scaffolder-backend.
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends python3 g++ build-essential && \
yarn config set python /usr/bin/python3
FROM node:18-bullseye-slim
# Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image,
# in which case you should also move better-sqlite3 to "devDependencies" in package.json.
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends libsqlite3-dev
apt-get install -y --no-install-recommends libsqlite3-dev python3 build-essential wget ca-certificates && \
yarn config set python /usr/bin/python3
RUN wget -P /usr/local/bin/ https://dl.k8s.io/release/v1.27.0/bin/linux/amd64/kubectl \
&& chmod +x /usr/local/bin/kubectl
# From here on we use the least-privileged `node` user to run the backend.
USER node
@ -43,10 +40,11 @@ COPY --chown=node:node yarn.lock package.json packages/backend/dist/skeleton.tar
RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz
RUN --mount=type=cache,target=/home/node/.cache/yarn,sharing=locked,uid=1000,gid=1000 \
--mount=type=secret,id=npmrc,target=./.npmrc,uid=1000 \
yarn install --frozen-lockfile --production --network-timeout 300000
# Then copy the rest of the backend bundle, along with any other files we might want.
COPY --chown=node:node packages/backend/dist/bundle.tar.gz app-config*.yaml ./
RUN tar xzf bundle.tar.gz && rm bundle.tar.gz
CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"]
CMD ["node", "packages/backend", "--config", "app-config.yaml"]

View file

@ -36,7 +36,7 @@ The backend starts up on port 7007 per default.
If you want to use the catalog functionality, you need to add so called
locations to the backend. These are places where the backend can find some
entity descriptor data to consume and serve. For more information, see
[Software Catalog Overview - Adding Components to the Catalog](https://backstage.io/docs/features/software-catalog/#adding-components-to-the-catalog).
[Software Catalog Overview - Adding Components to the Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview#adding-components-to-the-catalog).
To get started quickly, this template already includes some statically configured example locations
in `app-config.yaml` under `catalog.locations`. You can remove and replace these locations as you
@ -56,4 +56,4 @@ and
## Documentation
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
- [Backstage Documentation](https://backstage.io/docs)
- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md)

View file

@ -16,57 +16,44 @@
"build-image": "docker build ../.. -f Dockerfile --tag backstage"
},
"dependencies": {
"@backstage/backend-common": "^0.25.0",
"@backstage/backend-defaults": "^0.8.1",
"@backstage/backend-plugin-api": "^1.2.0",
"@backstage/backend-tasks": "^0.6.1",
"@backstage/catalog-client": "^1.9.1",
"@backstage/catalog-model": "^1.7.3",
"@backstage/config": "^1.3.2",
"@backstage/errors": "^1.2.7",
"@backstage/integration": "^1.16.1",
"@backstage/plugin-app-backend": "^0.4.5",
"@backstage/plugin-auth-backend": "^0.24.3",
"@backstage/plugin-auth-backend-module-guest-provider": "^0.2.5",
"@backstage/plugin-auth-backend-module-oidc-provider": "^0.4.0",
"@backstage/plugin-auth-node": "^0.6.0",
"@backstage/plugin-catalog-backend": "^1.31.0",
"@backstage/plugin-catalog-backend-module-scaffolder-entity-model": "^0.2.5",
"@backstage/plugin-kubernetes-backend": "^0.19.3",
"@backstage/plugin-permission-common": "^0.8.4",
"@backstage/plugin-permission-node": "^0.8.8",
"@backstage/plugin-proxy-backend": "^0.5.11",
"@backstage/plugin-scaffolder-backend": "^1.30.0",
"@backstage/plugin-scaffolder-backend-module-gitea": "^0.2.6",
"@backstage/plugin-scaffolder-backend-module-github": "^0.6.0",
"@backstage/plugin-scaffolder-node": "^0.7.0",
"@backstage/plugin-search-backend": "^1.8.2",
"@backstage/plugin-search-backend-module-catalog": "^0.3.1",
"@backstage/plugin-search-backend-module-pg": "^0.5.41",
"@backstage/plugin-search-backend-module-techdocs": "^0.3.6",
"@backstage/plugin-search-backend-node": "^1.3.8",
"@backstage/plugin-techdocs-backend": "^1.11.6",
"@backstage/types": "^1.2.1",
"@kubernetes/client-node": "~0.20.0",
"@roadiehq/backstage-plugin-argo-cd-backend": "3.1.0",
"@roadiehq/scaffolder-backend-module-http-request": "^4.3.5",
"@roadiehq/scaffolder-backend-module-utils": "3.0.0",
"@backstage/backend-common": "^0.19.1",
"@backstage/backend-tasks": "^0.5.4",
"@backstage/catalog-client": "^1.4.3",
"@backstage/catalog-model": "^1.4.1",
"@backstage/config": "^1.0.8",
"@backstage/integration": "^1.5.1",
"@backstage/plugin-app-backend": "^0.3.47",
"@backstage/plugin-auth-backend": "^0.18.5",
"@backstage/plugin-auth-node": "^0.2.16",
"@backstage/plugin-catalog-backend": "^1.11.0",
"@backstage/plugin-kubernetes-backend": "^0.11.2",
"@backstage/plugin-permission-common": "^0.7.7",
"@backstage/plugin-permission-node": "^0.7.10",
"@backstage/plugin-proxy-backend": "^0.2.41",
"@backstage/plugin-scaffolder-backend": "^1.15.1",
"@backstage/plugin-scaffolder-node": "^0.1.5",
"@backstage/plugin-search-backend": "^1.3.3",
"@backstage/plugin-search-backend-module-pg": "^0.5.8",
"@backstage/plugin-search-backend-node": "^1.2.3",
"@backstage/plugin-techdocs-backend": "^1.6.4",
"@backstage/types": "^1.1.0",
"@cnoe-io/plugin-scaffolder-actions": "0.1.1",
"@kubernetes/client-node": "^0.18.1",
"@roadiehq/scaffolder-backend-module-utils": "^1.8.7",
"app": "link:../app",
"better-sqlite3": "^9.0.0",
"better-sqlite3": "^8.0.0",
"dockerode": "^3.3.1",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"fs-extra": "~11.2.0",
"node-gyp": "^9.0.0",
"pg": "^8.11.3",
"winston": "^3.2.1"
"pg": "^8.3.0",
"winston": "^3.2.1",
"yaml": "^2.3.1"
},
"devDependencies": {
"@backstage/cli": "^0.30.0",
"@backstage/cli": "^0.22.9",
"@types/dockerode": "^3.3.0",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
"@types/fs-extra": "^11.0.4",
"@types/luxon": "^2.0.4"
},
"files": [

View file

@ -1,45 +1,120 @@
import { createBackend } from '@backstage/backend-defaults';
import { cnoeScaffolderActions } from './plugins/scaffolder';
import { authModuleKeycloakOIDCProvider } from './plugins/auth';
/*
* Hi!
*
* Note that this is an EXAMPLE Backstage backend. Please check the README.
*
* Happy hacking!
*/
const backend = createBackend();
import Router from 'express-promise-router';
import {
createServiceBuilder,
loadBackendConfig,
getRootLogger,
useHotMemoize,
notFoundHandler,
CacheManager,
DatabaseManager,
SingleHostDiscovery,
UrlReaders,
ServerTokenManager,
} from '@backstage/backend-common';
import { TaskScheduler } from '@backstage/backend-tasks';
import { Config } from '@backstage/config';
import app from './plugins/app';
import auth from './plugins/auth';
import catalog from './plugins/catalog';
import scaffolder from './plugins/scaffolder';
import proxy from './plugins/proxy';
import techdocs from './plugins/techdocs';
import search from './plugins/search';
import { PluginEnvironment } from './types';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
import kubernetes from './plugins/kubernetes';
// core plugins
backend.add(import('@backstage/plugin-app-backend'));
backend.add(import('@backstage/plugin-catalog-backend'));
backend.add(import('@backstage/plugin-proxy-backend'));
backend.add(import('@backstage/plugin-techdocs-backend/alpha'));
function makeCreateEnv(config: Config) {
const root = getRootLogger();
const reader = UrlReaders.default({ logger: root, config });
const discovery = SingleHostDiscovery.fromConfig(config);
const cacheManager = CacheManager.fromConfig(config);
const databaseManager = DatabaseManager.fromConfig(config, { logger: root });
const tokenManager = ServerTokenManager.noop();
const taskScheduler = TaskScheduler.fromConfig(config);
// auth plugins
backend.add(import('@backstage/plugin-auth-backend'));
backend.add(import('@backstage/plugin-auth-backend-module-guest-provider'));
const identity = DefaultIdentityClient.create({
discovery,
});
const permissions = ServerPermissionClient.fromConfig(config, {
discovery,
tokenManager,
});
// scaffolder plugins
backend.add(import('@backstage/plugin-scaffolder-backend/alpha'));
backend.add(
import('@backstage/plugin-catalog-backend-module-scaffolder-entity-model'),
);
backend.add(import('@backstage/plugin-scaffolder-backend-module-github'));
root.info(`Created UrlReader ${reader}`);
// search plugins
backend.add(import('@backstage/plugin-search-backend/alpha'));
return (plugin: string): PluginEnvironment => {
const logger = root.child({ type: 'plugin', plugin });
const database = databaseManager.forPlugin(plugin);
const cache = cacheManager.forPlugin(plugin);
const scheduler = taskScheduler.forPlugin(plugin);
return {
logger,
database,
cache,
config,
reader,
discovery,
tokenManager,
scheduler,
permissions,
identity,
};
};
}
backend.add(import('@backstage/plugin-search-backend-module-catalog'));
backend.add(import('@backstage/plugin-search-backend-module-techdocs/alpha'));
async function main() {
const config = await loadBackendConfig({
argv: process.argv,
logger: getRootLogger(),
});
const createEnv = makeCreateEnv(config);
// other @backstage plugins
backend.add(import('@backstage/plugin-kubernetes-backend'));
const catalogEnv = useHotMemoize(module, () => createEnv('catalog'));
const scaffolderEnv = useHotMemoize(module, () => createEnv('scaffolder'));
const authEnv = useHotMemoize(module, () => createEnv('auth'));
const proxyEnv = useHotMemoize(module, () => createEnv('proxy'));
const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs'));
const searchEnv = useHotMemoize(module, () => createEnv('search'));
const appEnv = useHotMemoize(module, () => createEnv('app'));
// roadie plugins
backend.add(import('@roadiehq/scaffolder-backend-module-utils/new-backend'));
backend.add(import('./plugins/argocd_index'));
const kubernetesEnv = useHotMemoize(module, () => createEnv('kubernetes'));
backend.add(
import('@roadiehq/scaffolder-backend-module-http-request/new-backend'),
);
const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv));
apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv));
apiRouter.use('/auth', await auth(authEnv));
apiRouter.use('/techdocs', await techdocs(techdocsEnv));
apiRouter.use('/proxy', await proxy(proxyEnv));
apiRouter.use('/search', await search(searchEnv));
// cnoe plugins
backend.add(authModuleKeycloakOIDCProvider);
backend.add(cnoeScaffolderActions);
apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv));
backend.start();
// Add backends ABOVE this line; this 404 handler is the catch-all fallback
apiRouter.use(notFoundHandler());
const service = createServiceBuilder(module)
.loadConfig(config)
.addRouter('/api', apiRouter)
.addRouter('', await app(appEnv));
await service.start().catch(err => {
console.log(err);
process.exit(1);
});
}
module.hot?.accept();
main().catch(error => {
console.error('Backend failed to start up', error);
process.exit(1);
});

View file

@ -0,0 +1,14 @@
import { createRouter } from '@backstage/plugin-app-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
return await createRouter({
logger: env.logger,
config: env.config,
database: env.database,
appPackageName: 'app',
});
}

View file

@ -1,174 +0,0 @@
import { Config } from '@backstage/config';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { examples } from './gitea-actions';
import { Logger } from 'winston';
import { ArgoService } from '@roadiehq/backstage-plugin-argo-cd-backend';
import { createRouter } from '@roadiehq/backstage-plugin-argo-cd-backend';
//import { PluginEnvironment } from '../types';
/*export default async function createPlugin({
logger,
config,
}: PluginEnvironment) {
return await createRouter({ logger, config });
}*/
import { loggerToWinstonLogger } from '@backstage/backend-common';
import {
coreServices,
createBackendPlugin,
} from '@backstage/backend-plugin-api';
export const argocdPlugin = createBackendPlugin({
pluginId: 'argocd',
register(env) {
env.registerInit({
deps: {
logger: coreServices.logger,
config: coreServices.rootConfig,
reader: coreServices.urlReader,
discovery: coreServices.discovery,
auth: coreServices.auth,
//tokenManager: coreServices.tokenManager,
httpRouter: coreServices.httpRouter,
},
async init({
logger,
config,
httpRouter,
}) {
httpRouter.use(
await createRouter({
logger: loggerToWinstonLogger(logger),
config,
}),
);
},
});
},
});
export function createArgoCDApp(options: { config: Config; logger: Logger }) {
const { config, logger } = options;
return createTemplateAction<{
repoUrl: string;
projectName?: string;
appName: string;
argoInstance: string;
path: string;
labelValue?: string;
appNamespace: string;
}>({
id: 'cnoe:create-argocd-app',
description: 'creates argocd app',
examples,
schema: {
input: {
type: 'object',
required: [
'repoUrl',
'projectName',
'appName',
'argoInstance',
'path',
'appNamespace',
],
properties: {
repoUrl: {
title: 'Repository Location',
type: 'string',
},
projectName: {
title: 'name of the project in argocd',
type: 'string',
},
appName: {
title: 'application name in argocd',
type: 'string',
},
appNamespace: {
title: 'application name in argocd',
type: 'string',
},
argoInstance: {
title: 'backstage argocd instance name defined in app-config.yaml',
type: 'string',
},
path: {
title: 'argocd spec path',
type: 'string',
},
labelValue: {
title: 'for argocd plugin to locate this app',
type: 'string',
},
},
},
output: {},
},
async handler(ctx) {
const {
repoUrl,
projectName,
appName,
argoInstance,
path,
labelValue,
appNamespace,
} = ctx.input;
const argoUserName =
config.getOptionalString('argocd.username') ?? 'argocdUsername';
const argoPassword =
config.getOptionalString('argocd.password') ?? 'argocdPassword';
const argoSvc = new ArgoService(
argoUserName,
argoPassword,
config,
logger,
);
const argocdConfig = config
.getConfigArray('argocd.appLocatorMethods')
.filter(element => element.getString('type') === 'config')
.reduce(
(acc: Config[], argoApp: Config) =>
acc.concat(argoApp.getConfigArray('instances')),
[],
)
.map(instance => ({
name: instance.getString('name'),
url: instance.getString('url'),
token: instance.getOptionalString('token'),
username: instance.getOptionalString('username'),
password: instance.getOptionalString('password'),
}));
const matchedArgoInstance = argocdConfig.find(
argoHost => argoHost.name === argoInstance,
);
if (!matchedArgoInstance) {
throw new Error(`Unable to find Argo instance named "${argoInstance}"`);
}
const token =
matchedArgoInstance.token ||
(await argoSvc.getArgoToken(matchedArgoInstance));
await argoSvc.createArgoApplication({
baseUrl: matchedArgoInstance.url,
argoToken: token,
appName: appName,
projectName: projectName ? projectName : appName,
namespace: appNamespace,
sourceRepo: repoUrl,
sourcePath: path,
labelValue: labelValue ? labelValue : appName,
});
},
});
}

View file

@ -1 +0,0 @@
export { argocdPlugin as default } from './argocd';

View file

@ -1,68 +1,57 @@
import {
createRouter,
providers,
defaultAuthProviderFactories,
} from '@backstage/plugin-auth-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
import {
DEFAULT_NAMESPACE,
stringifyEntityRef,
} from '@backstage/catalog-model';
import { JsonArray } from '@backstage/types';
import { createBackendModule } from '@backstage/backend-plugin-api';
import {
authProvidersExtensionPoint,
createOAuthProviderFactory,
OAuthAuthenticatorResult,
} from '@backstage/plugin-auth-node';
import {
oidcAuthenticator,
OidcAuthResult,
} from '@backstage/plugin-auth-backend-module-oidc-provider';
export const authModuleKeycloakOIDCProvider = createBackendModule({
pluginId: 'auth',
moduleId: 'keycloak-oidc',
register(reg) {
reg.registerInit({
deps: {
providers: authProvidersExtensionPoint,
},
async init({ providers }) {
providers.registerProvider({
providerId: 'keycloak-oidc',
factory: createOAuthProviderFactory({
authenticator: oidcAuthenticator,
profileTransform: async (
input: OAuthAuthenticatorResult<OidcAuthResult>,
) => ({
profile: {
email: input.fullProfile.userinfo.email,
picture: input.fullProfile.userinfo.picture,
displayName: input.fullProfile.userinfo.name,
},
}),
async signInResolver(info, ctx) {
const { profile } = info;
if (!profile.displayName) {
throw new Error(
'Login failed, user profile does not contain a valid name',
);
}
// should use users from catalog
const userRef = stringifyEntityRef({
kind: 'User',
name: info.profile.displayName!,
namespace: DEFAULT_NAMESPACE,
});
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const opts = {
logger: env.logger,
config: env.config,
database: env.database,
discovery: env.discovery,
tokenManager: env.tokenManager,
providerFactories: {
...defaultAuthProviderFactories,
},
};
return ctx.issueToken({
claims: {
sub: userRef,
ent: [userRef],
groups:
(info.result.fullProfile.userinfo.groups as JsonArray) ||
[],
},
});
const envName = env.config
.getOptionalConfig('auth')
?.getOptionalString('auth');
if (envName === 'local') {
return await createRouter(opts);
}
const keycloakAuth = (opts.providerFactories['keycloak-oidc'] =
providers.oidc.create({
signIn: {
resolver(info, ctx) {
const userRef = stringifyEntityRef({
kind: 'User',
name: info.result.userinfo.sub,
namespace: DEFAULT_NAMESPACE,
});
return ctx.issueToken({
claims: {
sub: userRef,
ent: [userRef],
groups: (info.result.userinfo.groups as JsonArray) || [],
},
}),
});
});
},
},
});
},
});
}));
opts.providerFactories['keycloak-oidc'] = keycloakAuth;
return await createRouter(opts);
}

View file

@ -0,0 +1,14 @@
import { CatalogBuilder } from '@backstage/plugin-catalog-backend';
import { ScaffolderEntitiesProcessor } from '@backstage/plugin-scaffolder-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const builder = await CatalogBuilder.create(env);
builder.addProcessor(new ScaffolderEntitiesProcessor());
const { processingEngine, router } = await builder.build();
await processingEngine.start();
return router;
}

View file

@ -1,43 +1,11 @@
import {
createTemplateAction,
executeShellCommand,
} from '@backstage/plugin-scaffolder-node';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { dumpYaml } from '@kubernetes/client-node';
import yaml from 'js-yaml';
import YAML from 'yaml';
import { Config } from '@backstage/config';
import { resolveSafeChildPath } from '@backstage/backend-common';
import fs from 'fs-extra';
import { executeShellCommand } from '@backstage/plugin-scaffolder-backend';
interface Cluster {
name: string;
cluster: {
server: string;
'insecure-skip-tls-verify': boolean;
'certificate-authority-data'?: string;
'certificate-authority'?: string;
};
}
interface Context {
name: string;
context: {
cluster: string;
user: string;
};
}
interface User {
name: string;
user: {
token?: string;
};
}
interface ConfFile {
apiVersion: string;
kind: string;
'current-context': string;
contexts: Context[];
clusters: Cluster[];
users: User[];
}
export const createKubernetesApply = (config: Config) => {
return createTemplateAction<{
manifestString?: string;
@ -89,34 +57,24 @@ export const createKubernetesApply = (config: Config) => {
},
},
async handler(ctx) {
let manifestPath = resolveSafeChildPath(
ctx.workspacePath,
'to-be-applied.yaml',
);
let obj: any;
if (ctx.input.manifestString) {
fs.writeFileSync(manifestPath, ctx.input.manifestString, {
encoding: 'utf8',
mode: '600',
});
obj = YAML.parse(ctx.input.manifestString);
} else if (ctx.input.manifestObject) {
fs.writeFileSync(manifestPath, yaml.dump(ctx.input.manifestObject), {
encoding: 'utf8',
mode: '600',
});
obj = ctx.input.manifestObject;
} else {
const filePath = resolveSafeChildPath(
ctx.workspacePath,
ctx.input.manifestPath!,
);
manifestPath = filePath;
const fileContent = fs.readFileSync(filePath, 'utf8');
obj = YAML.parse(fileContent);
}
const fileContent = fs.readFileSync(manifestPath, 'utf8');
const objList: any[] = yaml.loadAll(fileContent);
if (ctx.input.clusterName) {
// Supports SA token authentication only
const targetCluster = getClusterConfig(ctx.input.clusterName!, config);
const confFile: ConfFile = {
const confFile = {
apiVersion: 'v1',
kind: 'Config',
'current-context': ctx.input.clusterName,
@ -133,9 +91,8 @@ export const createKubernetesApply = (config: Config) => {
{
name: ctx.input.clusterName,
cluster: {
'certificate-authority-data': targetCluster.getString('caData'),
server: targetCluster.getString('url'),
'insecure-skip-tls-verify':
!!targetCluster.getOptionalBoolean('skipTLSVerify'),
},
},
],
@ -148,84 +105,43 @@ export const createKubernetesApply = (config: Config) => {
},
],
};
if (!confFile.clusters[0].cluster['insecure-skip-tls-verify']) {
let caDataRaw = targetCluster.getOptionalString('caData');
if (caDataRaw?.startsWith('-----BEGIN CERTIFICATE-----')) {
caDataRaw = Buffer.from(
targetCluster.getString('caData'),
'utf8',
).toString('base64');
}
confFile.clusters[0].cluster['certificate-authority-data'] =
caDataRaw;
if (
targetCluster.getOptionalString('caFile') &&
!(
targetCluster.getOptionalString('caFile')?.length === 0 ||
targetCluster.getOptionalString('caFile') === null
)
) {
confFile.clusters[0].cluster['certificate-authority'] =
targetCluster.getString('caFile');
}
}
if (
targetCluster
.getString('caData')
.startsWith('-----BEGIN CERTIFICATE-----')
) {
confFile.clusters[0].cluster['certificate-authority-data'] =
Buffer.from(targetCluster.getString('caData'), 'utf8').toString(
'base64',
);
}
const confString = dumpYaml(confFile);
const confFilePath = resolveSafeChildPath(ctx.workspacePath, 'config');
fs.writeFileSync(confFilePath, confString, {
encoding: 'utf8',
mode: '600',
});
await executeShellCommand({
command: 'cat',
args: [confFilePath],
logStream: ctx.logStream,
});
await executeShellCommand({
command: 'cat',
args: [manifestPath],
logStream: ctx.logStream,
});
let counter = 1;
for (const obj of objList) {
let manifestFilePath = resolveSafeChildPath(
ctx.workspacePath,
'to-be-applied-' + counter.toString() + '.yaml',
);
fs.writeFileSync(manifestFilePath, yaml.dump(obj), {
encoding: 'utf8',
mode: '600',
const manifestPath = resolveSafeChildPath(
ctx.workspacePath,
ctx.input.manifestPath!,
);
if (obj.metadata.generateName !== undefined) {
await executeShellCommand({
command: 'kubectl',
args: ['--kubeconfig', confFilePath, 'create', '-f', manifestPath],
logStream: ctx.logStream,
});
if (obj.metadata.generateName !== undefined) {
await executeShellCommand({
command: 'kubectl',
args: [
'--kubeconfig',
confFilePath,
'create',
'-f',
manifestFilePath,
],
logStream: ctx.logStream,
});
} else {
await executeShellCommand({
command: 'kubectl',
args: [
'--kubeconfig',
confFilePath,
'apply',
'-f',
manifestFilePath,
],
logStream: ctx.logStream,
});
}
counter += 1;
return;
}
await executeShellCommand({
command: 'kubectl',
args: ['--kubeconfig', confFilePath, 'apply', '-f', manifestPath],
logStream: ctx.logStream,
});
return;
}
throw new Error('please specify a valid cluster name');
return;
},
});
};
@ -252,4 +168,4 @@ function getClusterConfig(name: string, config: Config): Config {
throw new Error(`Cluster with name ${name} not found`);
}
return clusters[0];
}
}

View file

@ -1,600 +0,0 @@
import { InputError } from '@backstage/errors';
import { Config } from '@backstage/config';
import {
getGiteaRequestOptions,
GiteaIntegrationConfig,
ScmIntegrationRegistry,
ScmIntegrations,
} from '@backstage/integration';
import {
createTemplateAction,
getRepoSourceDirectory,
initRepoAndPush,
TemplateExample,
} from '@backstage/plugin-scaffolder-node';
import crypto from 'crypto';
import yaml from 'yaml';
export const examples: TemplateExample[] = [
{
description:
'Initializes a Gitea repository using the content of the workspace and publish it to Gitea with default configuration.',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
},
},
],
}),
},
{
description: 'Initializes a Gitea repository with a description.',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
description: 'Initialize a gitea repository',
},
},
],
}),
},
{
description:
'Initializes a Gitea repository with a default Branch, if not set defaults to main',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
defaultBranch: 'main',
},
},
],
}),
},
{
description:
'Initializes a Gitea repository with an initial commit message, if not set defaults to initial commit',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
gitCommitMessage: 'Initial Commit Message',
},
},
],
}),
},
{
description:
'Initializes a Gitea repository with a repo Author Name, if not set defaults to Scaffolder',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
gitAuthorName: 'John Doe',
},
},
],
}),
},
{
description: 'Initializes a Gitea repository with a repo Author Email',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
gitAuthorEmail: 'johndoe@email.com',
},
},
],
}),
},
{
description:
'Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
sourcePath: 'repository/',
},
},
],
}),
},
{
description: 'Initializes a Gitea repository with all properties being set',
example: yaml.stringify({
steps: [
{
id: 'publish',
action: 'publish:gitea',
name: 'Publish to Gitea',
input: {
repoUrl: 'gitea.com?repo=repo&owner=owner',
description: 'Initialize a gitea repository',
defaultBranch: 'staging',
gitCommitMessage: 'Initial Commit Message',
gitAuthorName: 'John Doe',
gitAuthorEmail: 'johndoe@email.com',
sourcePath: 'repository/',
},
},
],
}),
},
];
const parseRepoUrl = (
repoUrl: string,
integrations: ScmIntegrationRegistry,
): {
repo: string;
host: string;
owner?: string;
organization?: string;
workspace?: string;
project?: string;
} => {
let parsed;
try {
parsed = new URL(`https://${repoUrl}`);
} catch (error) {
throw new InputError(
`Invalid repo URL passed to publisher, got ${repoUrl}, ${error}`,
);
}
const host = parsed.host;
const owner = parsed.searchParams.get('owner') ?? undefined;
const organization = parsed.searchParams.get('organization') ?? undefined;
const workspace = parsed.searchParams.get('workspace') ?? undefined;
const project = parsed.searchParams.get('project') ?? undefined;
const type = integrations.byHost(host)?.type;
if (!type) {
throw new InputError(
`No matching integration configuration for host ${host}, please check your integrations config`,
);
}
const repo: string = parsed.searchParams.get('repo')!;
switch (type) {
case 'bitbucket': {
if (host === 'www.bitbucket.org') {
checkRequiredParams(parsed, 'workspace');
}
checkRequiredParams(parsed, 'project', 'repo');
break;
}
case 'gitlab': {
// project is the projectID, and if defined, owner and repo won't be needed.
if (!project) {
checkRequiredParams(parsed, 'owner', 'repo');
}
break;
}
case 'gitea': {
checkRequiredParams(parsed, 'repo');
break;
}
case 'gerrit': {
checkRequiredParams(parsed, 'repo');
break;
}
default: {
checkRequiredParams(parsed, 'repo', 'owner');
break;
}
}
return { host, owner, repo, organization, workspace, project };
};
function checkRequiredParams(repoUrl: URL, ...params: string[]) {
for (let i = 0; i < params.length; i++) {
if (!repoUrl.searchParams.get(params[i])) {
throw new InputError(
`Invalid repo URL passed to publisher: ${repoUrl.toString()}, missing ${
params[i]
}`,
);
}
}
}
// const checkGiteaContentUrl = async (
// config: GiteaIntegrationConfig,
// options: {
// owner?: string;
// repo: string;
// defaultBranch?: string;
// },
// ): Promise<Response> => {
// const { owner, repo, defaultBranch } = options;
// let response: Response;
// const getOptions: RequestInit = {
// method: 'GET',
// };
//
// try {
// response = await fetch(
// `${config.baseUrl}/${owner}/${repo}/src/branch/${defaultBranch}`,
// getOptions,
// );
// } catch (e) {
// throw new Error(
// `Unable to get the repository: ${owner}/${repo} metadata , ${e}`,
// );
// }
// return response;
// };
const checkGiteaOrg = async (
config: GiteaIntegrationConfig,
options: {
owner: string;
},
): Promise<void> => {
const { owner } = options;
let response: Response;
// check first if the org = owner exists
const getOptions: RequestInit = {
method: 'GET',
headers: {
...getGiteaRequestOptions(config).headers,
'Content-Type': 'application/json',
},
};
try {
response = await fetch(
`${config.baseUrl}/api/v1/orgs/${owner}`,
getOptions,
);
} catch (e) {
throw new Error(`Unable to get the Organization: ${owner}, ${e}`);
}
if (response.status !== 200) {
throw new Error(
`Organization ${owner} do not exist. Please create it first !`,
);
}
};
const createGiteaProject = async (
config: GiteaIntegrationConfig,
options: {
projectName: string;
owner?: string;
description: string;
},
): Promise<void> => {
const { projectName, description, owner } = options;
/*
Several options exist to create a repository using either the user or organisation
User: https://gitea.com/api/swagger#/user/createCurrentUserRepo
Api: URL/api/v1/user/repos
Remark: The user is the username defined part of the backstage integration config for the gitea URL !
Org: https://gitea.com/api/swagger#/organization/createOrgRepo
Api: URL/api/v1/orgs/${org_owner}/repos
This is the default scenario that we support currently
*/
let response: Response;
const postOptions: RequestInit = {
method: 'POST',
body: JSON.stringify({
name: projectName,
description,
}),
headers: {
...getGiteaRequestOptions(config).headers,
'Content-Type': 'application/json',
},
};
if (owner) {
try {
response = await fetch(
`${config.baseUrl}/api/v1/orgs/${owner}/repos`,
postOptions,
);
} catch (e) {
throw new Error(`Unable to create repository, ${e}`);
}
if (response.status !== 201) {
throw new Error(
`Unable to create repository, ${response.status} ${
response.statusText
}, ${await response.text()}`,
);
}
} else {
try {
response = await fetch(
`${config.baseUrl}/api/v1/user/repos`,
postOptions,
);
} catch (e) {
throw new Error(`Unable to create repository, ${e}`);
}
if (response.status !== 201) {
throw new Error(
`Unable to create repository, ${response.status} ${
response.statusText
}, ${await response.text()}`,
);
}
}
};
const generateCommitMessage = (
config: Config,
commitSubject?: string,
): string => {
const changeId = crypto.randomBytes(20).toString('hex');
const msg = `${
config.getOptionalString('scaffolder.defaultCommitMessage') || commitSubject
}\n\nChange-Id: I${changeId}`;
return msg;
};
// async function checkDurationLimit(fn: () => void, timeLimit: number): Promise<boolean> {
//
// const startTime = process.hrtime();
//
// // Call the function
// await fn();
//
// const endTime = process.hrtime(startTime);
// const durationInMs = endTime[0] * 1000 + endTime[1] / 1e6;
//
// // Check if the duration exceeds the time limit
// return durationInMs <= timeLimit;
// }
//
// async function checkAvailabilityGiteaRepository(
// integrationConfig: GiteaIntegrationConfig,
// options: {
// owner?: string;
// repo: string;
// defaultBranch: string;
// ctx: ActionContext<any>;
// },
// ) {
// const { owner, repo, defaultBranch, ctx } = options;
// const sleep = (ms: number | undefined) => new Promise(r => setTimeout(r, ms));
// let response: Response;
//
// const p = new Promise<void>((resolve, reject) => {
// setTimeout(async () => {
// response = await checkGiteaContentUrl(integrationConfig, {
// owner,
// repo,
// defaultBranch,
// });
//
// while (response.status !== 200) {
// if (ctx.signal?.aborted) return;
// await sleep(1000);
// response = await checkGiteaContentUrl(integrationConfig, {
// owner,
// repo,
// defaultBranch,
// });
// }
// resolve()
// },
// 5000
// )
// })
// return p
//
// }
/**
* Creates a new action that initializes a git repository using the content of the workspace.
* and publishes it to a Gitea instance.
* @public
*/
export function createPublishGiteaAction(options: {
integrations: ScmIntegrations;
config: Config;
}) {
const { integrations, config } = options;
return createTemplateAction<{
repoUrl: string;
description: string;
defaultBranch?: string;
gitCommitMessage?: string;
gitAuthorName?: string;
gitAuthorEmail?: string;
sourcePath?: string;
}>({
id: 'publish:gitea',
description:
'Initializes a git repository using the content of the workspace, and publishes it to Gitea.',
examples,
schema: {
input: {
type: 'object',
required: ['repoUrl'],
properties: {
repoUrl: {
title: 'Repository Location',
type: 'string',
},
description: {
title: 'Repository Description',
type: 'string',
},
defaultBranch: {
title: 'Default Branch',
type: 'string',
description: `Sets the default branch on the repository. The default value is 'main'`,
},
gitCommitMessage: {
title: 'Git Commit Message',
type: 'string',
description: `Sets the commit message on the repository. The default value is 'initial commit'`,
},
gitAuthorName: {
title: 'Default Author Name',
type: 'string',
description: `Sets the default author name for the commit. The default value is 'Scaffolder'`,
},
gitAuthorEmail: {
title: 'Default Author Email',
type: 'string',
description: `Sets the default author email for the commit.`,
},
sourcePath: {
title: 'Source Path',
type: 'string',
description: `Path within the workspace that will be used as the repository root. If omitted, the entire workspace will be published as the repository.`,
},
},
},
output: {
type: 'object',
properties: {
remoteUrl: {
title: 'A URL to the repository with the provider',
type: 'string',
},
repoContentsUrl: {
title: 'A URL to the root of the repository',
type: 'string',
},
commitHash: {
title: 'The git commit hash of the initial commit',
type: 'string',
},
},
},
},
async handler(ctx) {
const {
repoUrl,
description,
defaultBranch = 'main',
gitAuthorName,
gitAuthorEmail,
gitCommitMessage = 'initial commit',
sourcePath,
} = ctx.input;
const { repo, host, owner } = parseRepoUrl(repoUrl, integrations);
const integrationConfig = integrations.gitea.byHost(host);
if (!integrationConfig) {
throw new InputError(
`No matching integration configuration for host ${host}, please check your integrations config`,
);
}
const { username, password } = integrationConfig.config;
if (!username || !password) {
throw new Error('Credentials for the gitea ${host} required.');
}
// check if the org exists within the gitea server
if (owner && owner !== username) {
await checkGiteaOrg(integrationConfig.config, { owner });
}
await createGiteaProject(integrationConfig.config, {
description,
owner: owner,
projectName: repo,
});
const auth = {
username: username,
password: password,
};
const gitAuthorInfo = {
name: gitAuthorName
? gitAuthorName
: config.getOptionalString('scaffolder.defaultAuthor.name'),
email: gitAuthorEmail
? gitAuthorEmail
: config.getOptionalString('scaffolder.defaultAuthor.email'),
};
// The owner to be used should be either the org name or user authenticated with the gitea server
const repoOwner = owner ? owner : username;
const remoteUrl = `${integrationConfig.config.baseUrl}/${repoOwner}/${repo}.git`;
const commitResult = await initRepoAndPush({
dir: getRepoSourceDirectory(ctx.workspacePath, sourcePath),
remoteUrl,
auth,
defaultBranch,
logger: ctx.logger,
commitMessage: generateCommitMessage(config, gitCommitMessage),
gitAuthorInfo,
});
// Check if the gitea repo URL is available before to exit
const operationTimeLimit = 5000; // 20 seconds
const sleep = (ms: number | undefined) =>
new Promise(r => setTimeout(r, ms));
await sleep(operationTimeLimit);
// await checkAvailabilityGiteaRepository(
// integrationConfig.config, {
// repoOwner,
// repo,
// defaultBranch,
// ctx,
// }
// )
// const checkDuration = await checkDurationLimit(
// async () =>
// await checkAvailabilityGiteaRepository(integrationConfig.config, {
// repoOwner,
// repo,
// defaultBranch,
// ctx,
// }),
// operationTimeLimit,
// );
//
// if (!checkDuration) {
// console.log('Operation exceeded the time limit.');
// }
const repoContentsUrl = `${integrationConfig.config.baseUrl}/${repoOwner}/${repo}/src/branch/${defaultBranch}/`;
ctx.output('remoteUrl', remoteUrl);
ctx.output('commitHash', commitResult?.commitHash);
ctx.output('repoContentsUrl', repoContentsUrl);
},
});
}

View file

@ -0,0 +1,18 @@
import { KubernetesBuilder } from '@backstage/plugin-kubernetes-backend';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
import { CatalogClient } from '@backstage/catalog-client';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const catalogApi = new CatalogClient({ discoveryApi: env.discovery });
const { router } = await KubernetesBuilder.createBuilder({
logger: env.logger,
config: env.config,
catalogApi,
permissions: env.permissions,
}).build();
return router;
}

View file

@ -1,68 +0,0 @@
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import yaml from 'js-yaml';
// Add type annotations to fix TS2742
type SanitizeResourceInput = {
document: string;
};
type SanitizeResourceOutput = {
sanitized: string;
};
export const createSanitizeResource = () => {
return createTemplateAction<SanitizeResourceInput, SanitizeResourceOutput>({
id: 'cnoe:utils:sanitize',
schema: {
input: {
type: 'object',
required: ['document'],
properties: {
document: {
type: 'string',
title: 'Document',
description: 'The document to be sanitized',
},
},
},
output: {
type: 'object',
properties: {
sanitized: {
type: 'string',
description: 'The sanitized yaml string',
},
},
},
},
async handler(ctx) {
const obj = yaml.load(ctx.input.document);
ctx.output('sanitized', yaml.dump(removeEmptyObjects(obj)));
},
});
};
// Remove empty elements from an object
function removeEmptyObjects(obj: any): any {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
const newObj: any = Array.isArray(obj) ? [] : {};
for (const key in obj) {
const value = obj[key];
const newValue = removeEmptyObjects(value);
if (
!(
newValue === null ||
newValue === undefined ||
(typeof newValue === 'object' && Object.keys(newValue).length === 0)
)
) {
newObj[key] = newValue;
}
}
return newObj;
}

View file

@ -1,44 +1,82 @@
import { ScmIntegrations } from '@backstage/integration';
import { createPublishGiteaAction } from './gitea-actions';
import { CatalogClient } from '@backstage/catalog-client';
import {
coreServices,
createBackendModule,
} from '@backstage/backend-plugin-api';
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
import { createArgoCDApp } from './argocd';
import { getRootLogger } from '@backstage/backend-common';
import { createKubernetesApply } from './k8s-apply';
import { createSanitizeResource } from './sanitize';
import { createVerifyDependency } from './verify';
createBuiltinActions,
createRouter,
} from '@backstage/plugin-scaffolder-backend';
import { Router } from 'express';
import type { PluginEnvironment } from '../types';
import { ScmIntegrations } from '@backstage/integration';
import {
createZipAction,
createSleepAction,
createWriteFileAction,
createAppendFileAction,
createMergeJSONAction,
createMergeAction,
createParseFileAction,
createSerializeYamlAction,
createSerializeJsonAction,
createJSONataAction,
createYamlJSONataTransformAction,
createJsonJSONataTransformAction,
} from '@roadiehq/scaffolder-backend-module-utils';
import {
// createKubernetesApply,
createSanitizeResource,
createVerifyDependency,
} from '@cnoe-io/plugin-scaffolder-actions';
export const cnoeScaffolderActions = createBackendModule({
pluginId: 'scaffolder',
moduleId: 'cnoe-actions',
register(env) {
env.registerInit({
deps: {
scaffolder: scaffolderActionsExtensionPoint,
config: coreServices.rootConfig,
},
async init({ scaffolder, config }) {
const integrations = ScmIntegrations.fromConfig(config);
const logger = getRootLogger();
import { createKubernetesApply } from './cnoe-kube';
scaffolder.addActions(
createPublishGiteaAction({
integrations,
config,
}),
createArgoCDApp({
config,
logger,
}),
createKubernetesApply(config),
createSanitizeResource(),
createVerifyDependency(),
);
},
});
},
});
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
const catalogClient = new CatalogClient({
discoveryApi: env.discovery,
});
const integrations = ScmIntegrations.fromConfig(env.config);
const builtInActions = createBuiltinActions({
integrations,
catalogClient,
config: env.config,
reader: env.reader,
});
const scaffolderBackendModuleUtils = [
createZipAction(),
createSleepAction(),
createWriteFileAction(),
createAppendFileAction(),
createMergeJSONAction({}),
createMergeAction(),
createParseFileAction(),
createSerializeYamlAction(),
createSerializeJsonAction(),
createJSONataAction(),
createYamlJSONataTransformAction(),
createJsonJSONataTransformAction(),
];
const cnoeActions = [
createSanitizeResource(),
createVerifyDependency(),
createKubernetesApply(env.config),
];
const actions = [
...builtInActions,
...scaffolderBackendModuleUtils,
...cnoeActions,
];
return await createRouter({
actions: actions,
logger: env.logger,
config: env.config,
database: env.database,
reader: env.reader,
catalogClient: catalogClient,
identity: env.identity,
});
}

View file

@ -0,0 +1,66 @@
import { useHotCleanup } from '@backstage/backend-common';
import { createRouter } from '@backstage/plugin-search-backend';
import {
IndexBuilder,
LunrSearchEngine,
} from '@backstage/plugin-search-backend-node';
import { PluginEnvironment } from '../types';
import { DefaultCatalogCollatorFactory } from '@backstage/plugin-catalog-backend';
import { DefaultTechDocsCollatorFactory } from '@backstage/plugin-techdocs-backend';
import { Router } from 'express';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
// Initialize a connection to a search engine.
const searchEngine = new LunrSearchEngine({
logger: env.logger,
});
const indexBuilder = new IndexBuilder({
logger: env.logger,
searchEngine,
});
const schedule = env.scheduler.createScheduledTaskRunner({
frequency: { minutes: 10 },
timeout: { minutes: 15 },
// A 3 second delay gives the backend server a chance to initialize before
// any collators are executed, which may attempt requests against the API.
initialDelay: { seconds: 3 },
});
// Collators are responsible for gathering documents known to plugins. This
// collator gathers entities from the software catalog.
indexBuilder.addCollator({
schedule,
factory: DefaultCatalogCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
tokenManager: env.tokenManager,
}),
});
// collator gathers entities from techdocs.
indexBuilder.addCollator({
schedule,
factory: DefaultTechDocsCollatorFactory.fromConfig(env.config, {
discovery: env.discovery,
logger: env.logger,
tokenManager: env.tokenManager,
}),
});
// The scheduler controls when documents are gathered from collators and sent
// to the search engine for indexing.
const { scheduler } = await indexBuilder.build();
scheduler.start();
useHotCleanup(module, () => scheduler.stop());
return await createRouter({
engine: indexBuilder.getSearchEngine(),
types: indexBuilder.getDocumentTypes(),
permissions: env.permissions,
config: env.config,
logger: env.logger,
});
}

View file

@ -0,0 +1,51 @@
import { DockerContainerRunner } from '@backstage/backend-common';
import {
createRouter,
Generators,
Preparers,
Publisher,
} from '@backstage/plugin-techdocs-backend';
import Docker from 'dockerode';
import { Router } from 'express';
import { PluginEnvironment } from '../types';
export default async function createPlugin(
env: PluginEnvironment,
): Promise<Router> {
// Preparers are responsible for fetching source files for documentation.
const preparers = await Preparers.fromConfig(env.config, {
logger: env.logger,
reader: env.reader,
});
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
const containerRunner = new DockerContainerRunner({ dockerClient });
// Generators are used for generating documentation sites.
const generators = await Generators.fromConfig(env.config, {
logger: env.logger,
containerRunner,
});
// Publisher is used for
// 1. Publishing generated files to storage
// 2. Fetching files from storage and passing them to TechDocs frontend.
const publisher = await Publisher.fromConfig(env.config, {
logger: env.logger,
discovery: env.discovery,
});
// checks if the publisher is working and logs the result
await publisher.getReadiness();
return await createRouter({
preparers,
generators,
publisher,
logger: env.logger,
config: env.config,
discovery: env.discovery,
cache: env.cache,
});
}

View file

@ -1,69 +0,0 @@
import { executeShellCommand } from '@backstage/plugin-scaffolder-node';
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
import { Writable } from 'stream';
class ConsoleLogStream extends Writable {
data: string;
constructor(options: any) {
super(options);
this.data = '';
}
_write(chunk: any, _: any, callback: any) {
this.data += chunk.toString(); // Convert the chunk to a string and append it to this.data
console.log(this.data);
callback();
}
}
export const createVerifyDependency = () => {
return createTemplateAction<{
verifiers: string[];
}>({
id: 'cnoe:verify:dependency',
schema: {
input: {
type: 'object',
required: ['verifiers'],
properties: {
verifiers: {
type: 'array',
items: {
type: 'string',
},
title: 'verifiers',
description: 'The list of verifiers',
},
},
},
},
async handler(ctx) {
const verifiers = ctx.input.verifiers;
if (verifiers === null || verifiers.length === 0) {
ctx.logger.error('no verifier was supplied for the object');
return;
}
const baseCommand = 'cnoe';
const baseArguments = ['k8s', 'verify'];
verifiers.forEach((verifier: string) =>
baseArguments.push('--config', verifier),
);
const logStream = new ConsoleLogStream({});
await executeShellCommand({
command: baseCommand,
args: baseArguments,
logStream: logStream,
})
.then(() => ctx.logger.info('verification succeeded'))
.catch(error => {
ctx.logger.error(error);
throw new Error(logStream.data);
});
},
});
};

View file

@ -5,8 +5,9 @@ import {
PluginDatabaseManager,
PluginEndpointDiscovery,
TokenManager,
} from '@backstage/backend-common/dist'; //TODO: deprecated
import { PluginTaskScheduler } from '@backstage/backend-tasks/dist';
UrlReader,
} from '@backstage/backend-common';
import { PluginTaskScheduler } from '@backstage/backend-tasks';
import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import { IdentityApi } from '@backstage/plugin-auth-node';
@ -15,6 +16,7 @@ export type PluginEnvironment = {
database: PluginDatabaseManager;
cache: PluginCacheManager;
config: Config;
reader: UrlReader;
discovery: PluginEndpointDiscovery;
tokenManager: TokenManager;
scheduler: PluginTaskScheduler;

View file

@ -1,60 +0,0 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { defineConfig } from '@playwright/test';
import { generateProjects } from '@backstage/e2e-test-utils/playwright';
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
timeout: 60_000,
expect: {
timeout: 5_000,
},
// Run your local dev server before starting the tests
webServer: process.env.CI
? []
: [
{
command: 'yarn start',
port: 3000,
reuseExistingServer: true,
timeout: 60_000,
},
],
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: [['html', { open: 'never', outputFolder: 'e2e-test-report' }]],
use: {
actionTimeout: 0,
baseURL:
process.env.PLAYWRIGHT_URL ??
(process.env.CI ? 'http://localhost:7007' : 'http://localhost:3000'),
screenshot: 'only-on-failure',
trace: 'on-first-retry',
},
outputDir: 'node_modules/.cache/e2e-test-results',
projects: generateProjects(), // Find all packages with e2e-test folders
});

View file

@ -4,6 +4,6 @@ This is where your own plugins and their associated modules live, each in a
separate folder of its own.
If you want to create a new plugin here, go to your project root directory, run
the command `yarn new`, and follow the on-screen instructions.
the command `yarn backstage-cli create`, and follow the on-screen instructions.
You can also check out existing plugins on [the plugin marketplace](https://backstage.io/plugins)!

View file

@ -0,0 +1,53 @@
{
"name": "@internal/cnoe-ui-plugin",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "frontend-plugin"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/core-components": "^0.13.3",
"@backstage/core-plugin-api": "^1.5.3",
"@backstage/plugin-catalog-react": "^1.8.0",
"@backstage/plugin-home": "^0.5.4",
"@backstage/theme": "^0.4.1",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.22.9",
"@backstage/core-app-api": "^1.9.0",
"@backstage/dev-utils": "^1.0.17",
"@backstage/test-utils": "^1.4.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"@types/node": "*",
"cross-fetch": "^3.1.5",
"msw": "^0.49.0"
},
"files": [
"dist"
]
}

View file

@ -0,0 +1,83 @@
import { Content, Page } from '@backstage/core-components';
import { HomePageSearchBar } from '@backstage/plugin-search';
import { SearchContextProvider } from '@backstage/plugin-search-react';
import { Grid, makeStyles } from '@material-ui/core';
import React from 'react';
import {
HomePageToolkit,
HomePageCompanyLogo,
HomePageStarredEntities,
TemplateBackstageLogoIcon,
} from '@backstage/plugin-home';
import {
LogoBig,
} from './logos';
const useStyles = makeStyles(theme => ({
searchBar: {
display: 'flex',
maxWidth: '60vw',
backgroundColor: theme.palette.background.paper,
boxShadow: theme.shadows[1],
padding: '8px 0',
borderRadius: '50px',
margin: 'auto',
},
}));
const useLogoStyles = makeStyles(theme => ({
container: {
margin: theme.spacing(5, 0),
},
svg: {
width: 'auto',
height: 100,
},
path: {
fill: '#00568c',
},
}));
export const CNOEHomepage = () => {
const classes = useStyles();
const { container } = useLogoStyles();
return (
<SearchContextProvider>
<Page themeId="home">
<Content>
<Grid container justifyContent="center" spacing={6}>
<HomePageCompanyLogo className={container} logo={<LogoBig />} />
<Grid container item xs={12} alignItems="center" direction="row">
<HomePageSearchBar classes={{ root: classes.searchBar }} placeholder="Search" />
</Grid>
<Grid container item xs={12}>
<Grid item xs={12} md={6}>
<HomePageStarredEntities />
</Grid>
<Grid item xs={12} md={6}>
<HomePageToolkit
title="Quick Links"
tools={[
{
url: '/catalog',
label: 'Catalog',
icon: <TemplateBackstageLogoIcon/>,
},
{
url: '/docs',
label: 'Tech Docs',
icon: <TemplateBackstageLogoIcon />,
},
]}
/>
</Grid>
</Grid>
</Grid>
</Content>
</Page>
</SearchContextProvider>
);
};

View file

@ -0,0 +1,56 @@
import React from 'react';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles({
svg: {
width: 'auto',
height: 190,
},
whitePath: {
fill: '#ffffff',
stroke: 'none',
},
bluePath: {
fill: '#00568c',
stroke: 'none',
},
cyanPath: {
fill: '#00adee',
stroke: 'none',
},
});
export const LogoBig = () => {
const classes = useStyles();
return (
<svg
className={classes.svg}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2079.95 850.05"
>
<g><path className={classes.cyanPath} d="M 923.5,136.5 C 928.809,135.565 932.309,137.565 934,142.5C 947.791,206.068 948.457,269.735 936,333.5C 923.041,396.634 891.874,448.468 842.5,489C 832.527,492.024 828.694,488.524 831,478.5C 881.237,437.163 911.904,384.163 923,319.5C 929.867,281.3 931.201,242.966 927,204.5C 849.896,286.335 757.063,343.169 648.5,375C 560.306,401.762 470.306,412.428 378.5,407C 374.251,402.199 374.584,397.866 379.5,394C 493.995,398.085 604.329,378.751 710.5,336C 794.01,301.471 865.51,250.305 925,182.5C 924.752,176.101 923.752,169.768 922,163.5C 879.994,213.56 830.16,253.726 772.5,284C 654.362,345.618 528.696,374.618 395.5,371C 379.341,370.192 363.674,367.026 348.5,361.5C 354.504,394.851 367.67,425.184 388,452.5C 394,459.833 400.667,466.5 408,472.5C 410.323,482.507 406.489,486.007 396.5,483C 371.585,461.039 353.751,434.206 343,402.5C 336.28,383.953 331.28,364.953 328,345.5C 324.768,332.813 328.601,322.98 339.5,316C 350.829,310.223 362.829,307.89 375.5,309C 380.313,313.041 380.646,317.375 376.5,322C 366.116,323.176 356.116,325.843 346.5,330C 340.55,336.251 341.55,341.251 349.5,345C 365.81,351.729 382.81,355.396 400.5,356C 530.3,358.299 652.633,329.299 767.5,269C 829.235,236.299 881.235,192.132 923.5,136.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 659.5,164.5 C 714.308,178.702 768.974,193.536 823.5,209C 826.394,211.909 827.227,215.409 826,219.5C 823.588,222.792 820.421,223.959 816.5,223C 767.741,209.802 719.075,196.302 670.5,182.5C 669.668,192.483 669.168,202.483 669,212.5C 663.851,217.996 659.184,217.663 655,211.5C 654.333,197.5 654.333,183.5 655,169.5C 656.025,167.313 657.525,165.646 659.5,164.5 Z"/></g>
<g><path className={classes.bluePath} d="M 568.5,192.5 C 585.66,194.231 602.66,197.065 619.5,201C 635.167,211.333 650.833,221.667 666.5,232C 667.126,232.75 667.626,233.584 668,234.5C 668.667,257.167 668.667,279.833 668,302.5C 663.829,308.729 659.163,309.062 654,303.5C 653.833,282.825 653.333,262.159 652.5,241.5C 642.742,235.539 633.242,229.205 624,222.5C 623.667,253.833 623.333,285.167 623,316.5C 618.333,321.833 613.667,321.833 609,316.5C 608.5,282.502 608.333,248.502 608.5,214.5C 599.213,212.576 589.88,210.909 580.5,209.5C 580.667,248.168 580.5,286.835 580,325.5C 575.829,331.729 571.163,332.062 566,326.5C 565.667,288.833 565.333,251.167 565,213.5C 558.639,218.097 551.805,221.763 544.5,224.5C 537.956,223.089 535.789,219.089 538,212.5C 538.833,211.667 539.667,210.833 540.5,210C 550.066,204.392 559.399,198.559 568.5,192.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 490.5,212.5 C 504.026,214.715 517.36,217.881 530.5,222C 533.102,222.935 534.602,224.768 535,227.5C 535.667,263.167 535.667,298.833 535,334.5C 532.567,340.448 528.4,341.948 522.5,339C 521.107,337.829 520.273,336.329 520,334.5C 519.5,301.168 519.333,267.835 519.5,234.5C 512.102,232.981 504.769,231.481 497.5,230C 492.207,238.751 487.041,247.585 482,256.5C 481.667,285.833 481.333,315.167 481,344.5C 478.612,349.226 474.778,350.726 469.5,349C 468.667,348.167 467.833,347.333 467,346.5C 466.333,314.833 466.333,283.167 467,251.5C 474.342,238.14 482.175,225.14 490.5,212.5 Z"/></g>
<g><path className={classes.bluePath} d="M 432.5,231.5 C 442.172,231.334 451.839,231.5 461.5,232C 467.259,235.892 467.926,240.559 463.5,246C 456.254,247.391 448.921,247.891 441.5,247.5C 441.667,280.502 441.5,313.502 441,346.5C 437.004,351.3 432.67,351.633 428,347.5C 426.346,310.585 426.013,273.585 427,236.5C 428.107,233.887 429.94,232.22 432.5,231.5 Z"/></g>
<g><path className={classes.bluePath} d="M 392.5,265.5 C 400.507,265.334 408.507,265.5 416.5,266C 424.5,271 424.5,276 416.5,281C 411.511,281.499 406.511,281.666 401.5,281.5C 401.667,302.503 401.5,323.503 401,344.5C 396.333,349.833 391.667,349.833 387,344.5C 386.333,319.5 386.333,294.5 387,269.5C 388.5,267.531 390.333,266.198 392.5,265.5 Z"/></g>
<g><path className={classes.bluePath} d="M 898.5,452.5 C 942.835,452.333 987.168,452.5 1031.5,453C 1034.37,454.393 1035.7,456.726 1035.5,460C 1035.7,463.274 1034.37,465.607 1031.5,467C 987.167,467.667 942.833,467.667 898.5,467C 895.634,465.607 894.301,463.274 894.5,460C 894.43,456.634 895.763,454.134 898.5,452.5 Z"/></g>
<g><path className={classes.bluePath} d="M 243.5,464.5 C 285.501,464.333 327.501,464.5 369.5,465C 372.366,466.393 373.699,468.726 373.5,472C 373.699,475.274 372.366,477.607 369.5,479C 327.5,479.667 285.5,479.667 243.5,479C 240.484,477.471 239.151,474.971 239.5,471.5C 239.197,468.156 240.53,465.822 243.5,464.5 Z"/></g>
<g><path className={classes.bluePath} d="M 925.5,479.5 C 944.503,479.333 963.503,479.5 982.5,480C 988.509,484.314 988.843,488.981 983.5,494C 963.5,494.667 943.5,494.667 923.5,494C 919.002,488.32 919.669,483.487 925.5,479.5 Z"/></g>
<g><path className={classes.bluePath} d="M 235.5,490.5 C 260.502,490.333 285.502,490.5 310.5,491C 314.136,493.039 315.802,496.206 315.5,500.5C 404.501,500.333 493.501,500.5 582.5,501C 588.729,505.171 589.062,509.837 583.5,515C 490.167,515.667 396.833,515.667 303.5,515C 300.38,512.592 299.047,509.426 299.5,505.5C 277.831,505.667 256.164,505.5 234.5,505C 231.606,502.091 230.773,498.591 232,494.5C 232.69,492.65 233.856,491.316 235.5,490.5 Z"/></g>
<g><path className={classes.bluePath} d="M 614.5,498.5 C 714.167,498.333 813.834,498.5 913.5,499C 918.535,502.013 919.702,506.18 917,511.5C 916.25,512.126 915.416,512.626 914.5,513C 814.167,513.667 713.833,513.667 613.5,513C 608.003,507.683 608.336,502.85 614.5,498.5 Z"/></g>
<g><path className={classes.bluePath} d="M 566.5,526.5 C 588.169,526.333 609.836,526.5 631.5,527C 637.786,529.362 639.453,533.529 636.5,539.5C 635.335,540.584 634.002,541.417 632.5,542C 610.167,542.667 587.833,542.667 565.5,542C 560.774,539.612 559.274,535.778 561,530.5C 562.5,528.531 564.333,527.198 566.5,526.5 Z"/></g>
<g><path className={classes.bluePath} d="M 276.5,527.5 C 314.501,527.333 352.501,527.5 390.5,528C 396.552,532.223 396.885,536.889 391.5,542C 353.167,542.667 314.833,542.667 276.5,542C 272.788,539.487 271.622,535.987 273,531.5C 274.376,530.295 275.542,528.962 276.5,527.5 Z"/></g>
<g><path className={classes.bluePath} d="M 738.5,532.5 C 777.168,532.333 815.835,532.5 854.5,533C 860.062,538.163 859.729,542.829 853.5,547C 815.167,547.667 776.833,547.667 738.5,547C 735.634,545.607 734.301,543.274 734.5,540C 734.43,536.634 735.763,534.134 738.5,532.5 Z"/></g>
<g><path className={classes.bluePath} d="M 355.5,556.5 C 438.167,556.333 520.834,556.5 603.5,557C 609.075,562.115 608.742,566.781 602.5,571C 520.5,571.667 438.5,571.667 356.5,571C 350.367,566.613 350.033,561.78 355.5,556.5 Z"/></g>
<g><path className={classes.bluePath} d="M 844.5,558.5 C 880.502,558.333 916.502,558.5 952.5,559C 957.833,563.667 957.833,568.333 952.5,573C 916.833,573.667 881.167,573.667 845.5,573C 839.231,568.657 838.898,563.824 844.5,558.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 197.5,608.5 C 245.833,608.5 294.167,608.5 342.5,608.5C 342.5,622.5 342.5,636.5 342.5,650.5C 296.832,650.333 251.165,650.5 205.5,651C 192.103,652.4 183.603,659.566 180,672.5C 178,698.5 178,724.5 180,750.5C 183.451,762.947 191.618,770.113 204.5,772C 250.499,772.5 296.499,772.667 342.5,772.5C 342.5,786.5 342.5,800.5 342.5,814.5C 293.499,814.667 244.499,814.5 195.5,814C 161.97,812.133 142.803,794.633 138,761.5C 137.333,728.167 137.333,694.833 138,661.5C 140.962,636.219 154.462,619.719 178.5,612C 184.933,610.597 191.267,609.43 197.5,608.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 399.5,608.5 C 413.837,608.333 428.171,608.5 442.5,609C 483.022,655.888 523.189,703.054 563,750.5C 563.5,703.168 563.667,655.835 563.5,608.5C 577.5,608.5 591.5,608.5 605.5,608.5C 605.5,677.167 605.5,745.833 605.5,814.5C 591.163,814.667 576.829,814.5 562.5,814C 522.215,767.26 482.048,720.426 442,673.5C 441.5,720.499 441.333,767.499 441.5,814.5C 427.5,814.5 413.5,814.5 399.5,814.5C 399.5,745.833 399.5,677.167 399.5,608.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 722.5,608.5 C 753.86,608.071 785.193,608.571 816.5,610C 848.538,615.71 865.371,634.876 867,667.5C 867.667,696.833 867.667,726.167 867,755.5C 864.167,791.667 844.667,811.167 808.5,814C 778.833,814.667 749.167,814.667 719.5,814C 683.4,810.566 664.233,790.732 662,754.5C 661.333,725.833 661.333,697.167 662,668.5C 664.815,630.851 684.982,610.851 722.5,608.5 Z M 725.5,650.5 C 749.502,650.333 773.502,650.5 797.5,651C 813.546,652.713 822.712,661.546 825,677.5C 825.667,700.167 825.667,722.833 825,745.5C 822.833,761 814,769.833 798.5,772C 775.833,772.667 753.167,772.667 730.5,772C 714.546,769.712 705.713,760.546 704,744.5C 703.333,722.167 703.333,699.833 704,677.5C 704.565,662.953 711.732,653.953 725.5,650.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 924.5,608.5 C 992.833,608.5 1061.17,608.5 1129.5,608.5C 1129.5,622.5 1129.5,636.5 1129.5,650.5C 1061.17,650.5 992.833,650.5 924.5,650.5C 924.5,636.5 924.5,622.5 924.5,608.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 924.5,690.5 C 992.833,690.5 1061.17,690.5 1129.5,690.5C 1129.5,704.5 1129.5,718.5 1129.5,732.5C 1061.17,732.5 992.833,732.5 924.5,732.5C 924.5,718.5 924.5,704.5 924.5,690.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 924.5,772.5 C 992.833,772.5 1061.17,772.5 1129.5,772.5C 1129.5,786.5 1129.5,800.5 1129.5,814.5C 1061.17,814.5 992.833,814.5 924.5,814.5C 924.5,800.5 924.5,786.5 924.5,772.5 Z"/></g>
</svg>
);
};

View file

@ -0,0 +1,56 @@
import React from 'react';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles({
svg: {
width: 'auto',
height: 90,
},
whitePath: {
fill: '#ffffff',
stroke: 'none',
},
bluePath: {
fill: '#00568c',
stroke: 'none',
},
cyanPath: {
fill: '#00adee',
stroke: 'none',
},
});
export const LogoFull = () => {
const classes = useStyles();
return (
<svg
className={classes.svg}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 2079.95 850.05"
>
<g><path className={classes.cyanPath} d="M 923.5,136.5 C 928.809,135.565 932.309,137.565 934,142.5C 947.791,206.068 948.457,269.735 936,333.5C 923.041,396.634 891.874,448.468 842.5,489C 832.527,492.024 828.694,488.524 831,478.5C 881.237,437.163 911.904,384.163 923,319.5C 929.867,281.3 931.201,242.966 927,204.5C 849.896,286.335 757.063,343.169 648.5,375C 560.306,401.762 470.306,412.428 378.5,407C 374.251,402.199 374.584,397.866 379.5,394C 493.995,398.085 604.329,378.751 710.5,336C 794.01,301.471 865.51,250.305 925,182.5C 924.752,176.101 923.752,169.768 922,163.5C 879.994,213.56 830.16,253.726 772.5,284C 654.362,345.618 528.696,374.618 395.5,371C 379.341,370.192 363.674,367.026 348.5,361.5C 354.504,394.851 367.67,425.184 388,452.5C 394,459.833 400.667,466.5 408,472.5C 410.323,482.507 406.489,486.007 396.5,483C 371.585,461.039 353.751,434.206 343,402.5C 336.28,383.953 331.28,364.953 328,345.5C 324.768,332.813 328.601,322.98 339.5,316C 350.829,310.223 362.829,307.89 375.5,309C 380.313,313.041 380.646,317.375 376.5,322C 366.116,323.176 356.116,325.843 346.5,330C 340.55,336.251 341.55,341.251 349.5,345C 365.81,351.729 382.81,355.396 400.5,356C 530.3,358.299 652.633,329.299 767.5,269C 829.235,236.299 881.235,192.132 923.5,136.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 659.5,164.5 C 714.308,178.702 768.974,193.536 823.5,209C 826.394,211.909 827.227,215.409 826,219.5C 823.588,222.792 820.421,223.959 816.5,223C 767.741,209.802 719.075,196.302 670.5,182.5C 669.668,192.483 669.168,202.483 669,212.5C 663.851,217.996 659.184,217.663 655,211.5C 654.333,197.5 654.333,183.5 655,169.5C 656.025,167.313 657.525,165.646 659.5,164.5 Z"/></g>
<g><path className={classes.bluePath} d="M 568.5,192.5 C 585.66,194.231 602.66,197.065 619.5,201C 635.167,211.333 650.833,221.667 666.5,232C 667.126,232.75 667.626,233.584 668,234.5C 668.667,257.167 668.667,279.833 668,302.5C 663.829,308.729 659.163,309.062 654,303.5C 653.833,282.825 653.333,262.159 652.5,241.5C 642.742,235.539 633.242,229.205 624,222.5C 623.667,253.833 623.333,285.167 623,316.5C 618.333,321.833 613.667,321.833 609,316.5C 608.5,282.502 608.333,248.502 608.5,214.5C 599.213,212.576 589.88,210.909 580.5,209.5C 580.667,248.168 580.5,286.835 580,325.5C 575.829,331.729 571.163,332.062 566,326.5C 565.667,288.833 565.333,251.167 565,213.5C 558.639,218.097 551.805,221.763 544.5,224.5C 537.956,223.089 535.789,219.089 538,212.5C 538.833,211.667 539.667,210.833 540.5,210C 550.066,204.392 559.399,198.559 568.5,192.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 490.5,212.5 C 504.026,214.715 517.36,217.881 530.5,222C 533.102,222.935 534.602,224.768 535,227.5C 535.667,263.167 535.667,298.833 535,334.5C 532.567,340.448 528.4,341.948 522.5,339C 521.107,337.829 520.273,336.329 520,334.5C 519.5,301.168 519.333,267.835 519.5,234.5C 512.102,232.981 504.769,231.481 497.5,230C 492.207,238.751 487.041,247.585 482,256.5C 481.667,285.833 481.333,315.167 481,344.5C 478.612,349.226 474.778,350.726 469.5,349C 468.667,348.167 467.833,347.333 467,346.5C 466.333,314.833 466.333,283.167 467,251.5C 474.342,238.14 482.175,225.14 490.5,212.5 Z"/></g>
<g><path className={classes.bluePath} d="M 432.5,231.5 C 442.172,231.334 451.839,231.5 461.5,232C 467.259,235.892 467.926,240.559 463.5,246C 456.254,247.391 448.921,247.891 441.5,247.5C 441.667,280.502 441.5,313.502 441,346.5C 437.004,351.3 432.67,351.633 428,347.5C 426.346,310.585 426.013,273.585 427,236.5C 428.107,233.887 429.94,232.22 432.5,231.5 Z"/></g>
<g><path className={classes.bluePath} d="M 392.5,265.5 C 400.507,265.334 408.507,265.5 416.5,266C 424.5,271 424.5,276 416.5,281C 411.511,281.499 406.511,281.666 401.5,281.5C 401.667,302.503 401.5,323.503 401,344.5C 396.333,349.833 391.667,349.833 387,344.5C 386.333,319.5 386.333,294.5 387,269.5C 388.5,267.531 390.333,266.198 392.5,265.5 Z"/></g>
<g><path className={classes.bluePath} d="M 898.5,452.5 C 942.835,452.333 987.168,452.5 1031.5,453C 1034.37,454.393 1035.7,456.726 1035.5,460C 1035.7,463.274 1034.37,465.607 1031.5,467C 987.167,467.667 942.833,467.667 898.5,467C 895.634,465.607 894.301,463.274 894.5,460C 894.43,456.634 895.763,454.134 898.5,452.5 Z"/></g>
<g><path className={classes.bluePath} d="M 243.5,464.5 C 285.501,464.333 327.501,464.5 369.5,465C 372.366,466.393 373.699,468.726 373.5,472C 373.699,475.274 372.366,477.607 369.5,479C 327.5,479.667 285.5,479.667 243.5,479C 240.484,477.471 239.151,474.971 239.5,471.5C 239.197,468.156 240.53,465.822 243.5,464.5 Z"/></g>
<g><path className={classes.bluePath} d="M 925.5,479.5 C 944.503,479.333 963.503,479.5 982.5,480C 988.509,484.314 988.843,488.981 983.5,494C 963.5,494.667 943.5,494.667 923.5,494C 919.002,488.32 919.669,483.487 925.5,479.5 Z"/></g>
<g><path className={classes.bluePath} d="M 235.5,490.5 C 260.502,490.333 285.502,490.5 310.5,491C 314.136,493.039 315.802,496.206 315.5,500.5C 404.501,500.333 493.501,500.5 582.5,501C 588.729,505.171 589.062,509.837 583.5,515C 490.167,515.667 396.833,515.667 303.5,515C 300.38,512.592 299.047,509.426 299.5,505.5C 277.831,505.667 256.164,505.5 234.5,505C 231.606,502.091 230.773,498.591 232,494.5C 232.69,492.65 233.856,491.316 235.5,490.5 Z"/></g>
<g><path className={classes.bluePath} d="M 614.5,498.5 C 714.167,498.333 813.834,498.5 913.5,499C 918.535,502.013 919.702,506.18 917,511.5C 916.25,512.126 915.416,512.626 914.5,513C 814.167,513.667 713.833,513.667 613.5,513C 608.003,507.683 608.336,502.85 614.5,498.5 Z"/></g>
<g><path className={classes.bluePath} d="M 566.5,526.5 C 588.169,526.333 609.836,526.5 631.5,527C 637.786,529.362 639.453,533.529 636.5,539.5C 635.335,540.584 634.002,541.417 632.5,542C 610.167,542.667 587.833,542.667 565.5,542C 560.774,539.612 559.274,535.778 561,530.5C 562.5,528.531 564.333,527.198 566.5,526.5 Z"/></g>
<g><path className={classes.bluePath} d="M 276.5,527.5 C 314.501,527.333 352.501,527.5 390.5,528C 396.552,532.223 396.885,536.889 391.5,542C 353.167,542.667 314.833,542.667 276.5,542C 272.788,539.487 271.622,535.987 273,531.5C 274.376,530.295 275.542,528.962 276.5,527.5 Z"/></g>
<g><path className={classes.bluePath} d="M 738.5,532.5 C 777.168,532.333 815.835,532.5 854.5,533C 860.062,538.163 859.729,542.829 853.5,547C 815.167,547.667 776.833,547.667 738.5,547C 735.634,545.607 734.301,543.274 734.5,540C 734.43,536.634 735.763,534.134 738.5,532.5 Z"/></g>
<g><path className={classes.bluePath} d="M 355.5,556.5 C 438.167,556.333 520.834,556.5 603.5,557C 609.075,562.115 608.742,566.781 602.5,571C 520.5,571.667 438.5,571.667 356.5,571C 350.367,566.613 350.033,561.78 355.5,556.5 Z"/></g>
<g><path className={classes.bluePath} d="M 844.5,558.5 C 880.502,558.333 916.502,558.5 952.5,559C 957.833,563.667 957.833,568.333 952.5,573C 916.833,573.667 881.167,573.667 845.5,573C 839.231,568.657 838.898,563.824 844.5,558.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 197.5,608.5 C 245.833,608.5 294.167,608.5 342.5,608.5C 342.5,622.5 342.5,636.5 342.5,650.5C 296.832,650.333 251.165,650.5 205.5,651C 192.103,652.4 183.603,659.566 180,672.5C 178,698.5 178,724.5 180,750.5C 183.451,762.947 191.618,770.113 204.5,772C 250.499,772.5 296.499,772.667 342.5,772.5C 342.5,786.5 342.5,800.5 342.5,814.5C 293.499,814.667 244.499,814.5 195.5,814C 161.97,812.133 142.803,794.633 138,761.5C 137.333,728.167 137.333,694.833 138,661.5C 140.962,636.219 154.462,619.719 178.5,612C 184.933,610.597 191.267,609.43 197.5,608.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 399.5,608.5 C 413.837,608.333 428.171,608.5 442.5,609C 483.022,655.888 523.189,703.054 563,750.5C 563.5,703.168 563.667,655.835 563.5,608.5C 577.5,608.5 591.5,608.5 605.5,608.5C 605.5,677.167 605.5,745.833 605.5,814.5C 591.163,814.667 576.829,814.5 562.5,814C 522.215,767.26 482.048,720.426 442,673.5C 441.5,720.499 441.333,767.499 441.5,814.5C 427.5,814.5 413.5,814.5 399.5,814.5C 399.5,745.833 399.5,677.167 399.5,608.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 722.5,608.5 C 753.86,608.071 785.193,608.571 816.5,610C 848.538,615.71 865.371,634.876 867,667.5C 867.667,696.833 867.667,726.167 867,755.5C 864.167,791.667 844.667,811.167 808.5,814C 778.833,814.667 749.167,814.667 719.5,814C 683.4,810.566 664.233,790.732 662,754.5C 661.333,725.833 661.333,697.167 662,668.5C 664.815,630.851 684.982,610.851 722.5,608.5 Z M 725.5,650.5 C 749.502,650.333 773.502,650.5 797.5,651C 813.546,652.713 822.712,661.546 825,677.5C 825.667,700.167 825.667,722.833 825,745.5C 822.833,761 814,769.833 798.5,772C 775.833,772.667 753.167,772.667 730.5,772C 714.546,769.712 705.713,760.546 704,744.5C 703.333,722.167 703.333,699.833 704,677.5C 704.565,662.953 711.732,653.953 725.5,650.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 924.5,608.5 C 992.833,608.5 1061.17,608.5 1129.5,608.5C 1129.5,622.5 1129.5,636.5 1129.5,650.5C 1061.17,650.5 992.833,650.5 924.5,650.5C 924.5,636.5 924.5,622.5 924.5,608.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 924.5,690.5 C 992.833,690.5 1061.17,690.5 1129.5,690.5C 1129.5,704.5 1129.5,718.5 1129.5,732.5C 1061.17,732.5 992.833,732.5 924.5,732.5C 924.5,718.5 924.5,704.5 924.5,690.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 924.5,772.5 C 992.833,772.5 1061.17,772.5 1129.5,772.5C 1129.5,786.5 1129.5,800.5 1129.5,814.5C 1061.17,814.5 992.833,814.5 924.5,814.5C 924.5,800.5 924.5,786.5 924.5,772.5 Z"/></g>
</svg>
);
};

View file

@ -0,0 +1,44 @@
import React from 'react';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles({
svg: {
width: 'auto',
height: 28,
},
whitePath: {
fill: '#ffffff',
stroke: 'none',
},
bluePath: {
fill: '#00568c',
stroke: 'none',
},
cyanPath: {
fill: '#00adee',
stroke: 'none',
},
});
export const LogoIcon = () => {
const classes = useStyles();
return (
<svg
className={classes.svg}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 337.46 428.5"
>
<g><path className={classes.cyanPath} d="M 923.5,136.5 C 928.809,135.565 932.309,137.565 934,142.5C 947.791,206.068 948.457,269.735 936,333.5C 923.041,396.634 891.874,448.468 842.5,489C 832.527,492.024 828.694,488.524 831,478.5C 881.237,437.163 911.904,384.163 923,319.5C 929.867,281.3 931.201,242.966 927,204.5C 849.896,286.335 757.063,343.169 648.5,375C 560.306,401.762 470.306,412.428 378.5,407C 374.251,402.199 374.584,397.866 379.5,394C 493.995,398.085 604.329,378.751 710.5,336C 794.01,301.471 865.51,250.305 925,182.5C 924.752,176.101 923.752,169.768 922,163.5C 879.994,213.56 830.16,253.726 772.5,284C 654.362,345.618 528.696,374.618 395.5,371C 379.341,370.192 363.674,367.026 348.5,361.5C 354.504,394.851 367.67,425.184 388,452.5C 394,459.833 400.667,466.5 408,472.5C 410.323,482.507 406.489,486.007 396.5,483C 371.585,461.039 353.751,434.206 343,402.5C 336.28,383.953 331.28,364.953 328,345.5C 324.768,332.813 328.601,322.98 339.5,316C 350.829,310.223 362.829,307.89 375.5,309C 380.313,313.041 380.646,317.375 376.5,322C 366.116,323.176 356.116,325.843 346.5,330C 340.55,336.251 341.55,341.251 349.5,345C 365.81,351.729 382.81,355.396 400.5,356C 530.3,358.299 652.633,329.299 767.5,269C 829.235,236.299 881.235,192.132 923.5,136.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 659.5,164.5 C 714.308,178.702 768.974,193.536 823.5,209C 826.394,211.909 827.227,215.409 826,219.5C 823.588,222.792 820.421,223.959 816.5,223C 767.741,209.802 719.075,196.302 670.5,182.5C 669.668,192.483 669.168,202.483 669,212.5C 663.851,217.996 659.184,217.663 655,211.5C 654.333,197.5 654.333,183.5 655,169.5C 656.025,167.313 657.525,165.646 659.5,164.5 Z"/></g>
<g><path className={classes.bluePath} d="M 568.5,192.5 C 585.66,194.231 602.66,197.065 619.5,201C 635.167,211.333 650.833,221.667 666.5,232C 667.126,232.75 667.626,233.584 668,234.5C 668.667,257.167 668.667,279.833 668,302.5C 663.829,308.729 659.163,309.062 654,303.5C 653.833,282.825 653.333,262.159 652.5,241.5C 642.742,235.539 633.242,229.205 624,222.5C 623.667,253.833 623.333,285.167 623,316.5C 618.333,321.833 613.667,321.833 609,316.5C 608.5,282.502 608.333,248.502 608.5,214.5C 599.213,212.576 589.88,210.909 580.5,209.5C 580.667,248.168 580.5,286.835 580,325.5C 575.829,331.729 571.163,332.062 566,326.5C 565.667,288.833 565.333,251.167 565,213.5C 558.639,218.097 551.805,221.763 544.5,224.5C 537.956,223.089 535.789,219.089 538,212.5C 538.833,211.667 539.667,210.833 540.5,210C 550.066,204.392 559.399,198.559 568.5,192.5 Z"/></g>
<g><path className={classes.cyanPath} d="M 490.5,212.5 C 504.026,214.715 517.36,217.881 530.5,222C 533.102,222.935 534.602,224.768 535,227.5C 535.667,263.167 535.667,298.833 535,334.5C 532.567,340.448 528.4,341.948 522.5,339C 521.107,337.829 520.273,336.329 520,334.5C 519.5,301.168 519.333,267.835 519.5,234.5C 512.102,232.981 504.769,231.481 497.5,230C 492.207,238.751 487.041,247.585 482,256.5C 481.667,285.833 481.333,315.167 481,344.5C 478.612,349.226 474.778,350.726 469.5,349C 468.667,348.167 467.833,347.333 467,346.5C 466.333,314.833 466.333,283.167 467,251.5C 474.342,238.14 482.175,225.14 490.5,212.5 Z"/></g>
<g><path className={classes.bluePath} d="M 432.5,231.5 C 442.172,231.334 451.839,231.5 461.5,232C 467.259,235.892 467.926,240.559 463.5,246C 456.254,247.391 448.921,247.891 441.5,247.5C 441.667,280.502 441.5,313.502 441,346.5C 437.004,351.3 432.67,351.633 428,347.5C 426.346,310.585 426.013,273.585 427,236.5C 428.107,233.887 429.94,232.22 432.5,231.5 Z"/></g>
<g><path className={classes.bluePath} d="M 392.5,265.5 C 400.507,265.334 408.507,265.5 416.5,266C 424.5,271 424.5,276 416.5,281C 411.511,281.499 406.511,281.666 401.5,281.5C 401.667,302.503 401.5,323.503 401,344.5C 396.333,349.833 391.667,349.833 387,344.5C 386.333,319.5 386.333,294.5 387,269.5C 388.5,267.531 390.333,266.198 392.5,265.5 Z"/></g>
<g><path className={classes.bluePath} d="M 898.5,452.5 C 942.835,452.333 987.168,452.5 1031.5,453C 1034.37,454.393 1035.7,456.726 1035.5,460C 1035.7,463.274 1034.37,465.607 1031.5,467C 987.167,467.667 942.833,467.667 898.5,467C 895.634,465.607 894.301,463.274 894.5,460C 894.43,456.634 895.763,454.134 898.5,452.5 Z"/></g>
<g><path className={classes.bluePath} d="M 243.5,464.5 C 285.501,464.333 327.501,464.5 369.5,465C 372.366,466.393 373.699,468.726 373.5,472C 373.699,475.274 372.366,477.607 369.5,479C 327.5,479.667 285.5,479.667 243.5,479C 240.484,477.471 239.151,474.971 239.5,471.5C 239.197,468.156 240.53,465.822 243.5,464.5 Z"/></g>
<g><path className={classes.bluePath} d="M 925.5,479.5 C 944.503,479.333 963.503,479.5 982.5,480C 988.509,484.314 988.843,488.981 983.5,494C 963.5,494.667 943.5,494.667 923.5,494C 919.002,488.32 919.669,483.487 925.5,479.5 Z"/></g>
<g><path className={classes.bluePath} d="M 235.5,490.5 C 260.502,490.333 285.502,490.5 310.5,491C 314.136,493.039 315.802,496.206 315.5,500.5C 404.501,500.333 493.501,500.5 582.5,501C 588.729,505.171 589.062,509.837 583.5,515C 490.167,515.667 396.833,515.667 303.5,515C 300.38,512.592 299.047,509.426 299.5,505.5C 277.831,505.667 256.164,505.5 234.5,505C 231.606,502.091 230.773,498.591 232,494.5C 232.69,492.65 233.856,491.316 235.5,490.5 Z"/></g>
</svg>
);
};

View file

@ -0,0 +1,3 @@
export {LogoFull} from './LogoFull';
export {LogoIcon} from './LogoIcon';
export {LogoBig} from './LogoBig';

View file

@ -0,0 +1,38 @@
import {createTheme, darkTheme, genPageTheme, shapes} from '@backstage/theme';
export const cnoeDarkTheme = createTheme({
palette: {
...darkTheme.palette,
primary: {
main: '#25a0c2',
},
secondary: {
main: '#00568c',
},
},
defaultPageTheme: 'home',
pageTheme: {
home: genPageTheme({colors: ['#25a0c2', '#00568c'], shape: shapes.wave}),
documentation: genPageTheme({
colors: ['#25a0c2', '#00568c'],
shape: shapes.wave2,
}),
tool: genPageTheme({colors: ['#25a0c2', '#00568c'], shape: shapes.round}),
service: genPageTheme({
colors: ['#25a0c2', '#00568c'],
shape: shapes.wave,
}),
website: genPageTheme({
colors: ['#25a0c2', '#00568c'],
shape: shapes.wave,
}),
library: genPageTheme({
colors: ['#25a0c2', '#00568c'],
shape: shapes.wave,
}),
other: genPageTheme({colors: ['#25a0c2', '#00568c'], shape: shapes.wave}),
app: genPageTheme({colors: ['#25a0c2', '#00568c'], shape: shapes.wave}),
apis: genPageTheme({colors: ['#25a0c2', '#00568c'], shape: shapes.wave}),
},
});

View file

@ -0,0 +1,2 @@
export {cnoeDarkTheme} from './dark-theme';
export {cnoeLightTheme} from './light-theme';

View file

@ -0,0 +1,39 @@
import {createTheme, lightTheme, genPageTheme, shapes} from '@backstage/theme';
export const cnoeLightTheme = createTheme({
palette: {
...lightTheme.palette,
primary: {
main: '#00568c',
},
secondary: {
main: '#00adee',
},
},
defaultPageTheme: 'home',
pageTheme: {
home: genPageTheme({colors: ['#00568c', '#00adee'], shape: shapes.wave}),
documentation: genPageTheme({
colors: ['#00568c', '#00adee'],
shape: shapes.wave2,
}),
tool: genPageTheme({colors: ['#00568c', '#00adee'], shape: shapes.round}),
service: genPageTheme({
colors: ['#00568c', '#00adee'],
shape: shapes.wave,
}),
website: genPageTheme({
colors: ['#00568c', '#00adee'],
shape: shapes.wave,
}),
library: genPageTheme({
colors: ['#00568c', '#00adee'],
shape: shapes.wave,
}),
other: genPageTheme({colors: ['#00568c', '#00adee'], shape: shapes.wave}),
app: genPageTheme({colors: ['#00568c', '#00adee'], shape: shapes.wave}),
apis: genPageTheme({colors: ['#00568c', '#00adee'], shape: shapes.wave}),
},
});

View file

@ -0,0 +1,3 @@
export * from './components/themes';
export {LogoFull, LogoIcon} from './components/logos';
export {CNOEHomepage} from './components/Homepage';

View file

@ -0,0 +1,36 @@
import {createPlugin, createComponentExtension} from '@backstage/core-plugin-api';
import {rootRouteRef} from './routes';
export const cnoeFrontendPlugin = createPlugin({
id: 'cnoe-ui-plugin',
routes: {
root: rootRouteRef,
},
});
export const AWSLogoFull = cnoeFrontendPlugin
.provide(
createComponentExtension({
name: 'LogoFull',
component: {lazy: () => import('./components/logos/LogoFull').then(m => m.LogoFull)},
}),
);
export const AWSLogoIcon = cnoeFrontendPlugin
.provide(
createComponentExtension({
name: 'LogoIcon',
component: {lazy: () => import('./components/logos/LogoIcon').then(m => m.LogoIcon)},
}),
);
export const CNOEHomepage = cnoeFrontendPlugin
.provide(
createComponentExtension({
name: 'Homepage',
component: {
lazy: () => import('./components/Homepage').then(m => m.CNOEHomepage),
},
}),
);

View file

@ -0,0 +1,5 @@
import {createRouteRef} from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'cnoe-ui',
});

View file

@ -0,0 +1 @@
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);

View file

@ -0,0 +1,13 @@
# workflows
Welcome to the workflows plugin!
_This plugin was created through the Backstage CLI_
## Getting started
Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/workflows](http://localhost:3000/workflows).
You can also serve the plugin in isolation by running `yarn start` in the plugin directory.
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](./dev) directory.

View file

@ -0,0 +1,53 @@
{
"name": "@internal/plugin-workflows",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": true,
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
"role": "frontend-plugin"
},
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
"lint": "backstage-cli package lint",
"test": "backstage-cli package test",
"clean": "backstage-cli package clean",
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/core-components": "^0.13.3",
"@backstage/core-plugin-api": "^1.5.3",
"@backstage/plugin-catalog-react": "^1.8.0",
"@backstage/theme": "^0.4.1",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.22.9",
"@backstage/core-app-api": "^1.9.0",
"@backstage/dev-utils": "^1.0.17",
"@backstage/test-utils": "^1.4.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^14.0.0",
"@types/node": "*",
"cross-fetch": "^3.1.5",
"msw": "^0.49.0"
},
"files": [
"dist"
]
}

View file

@ -0,0 +1,26 @@
// import React from 'react';
// import { rest } from 'msw';
// import { setupServer } from 'msw/node';
// import { screen } from '@testing-library/react';
// import {
// setupRequestMockHandlers,
// renderInTestApp,
// } from "@backstage/test-utils";
// describe('ExampleComponent', () => {
// const server = setupServer();
// // Enable sane handlers for network requests
// setupRequestMockHandlers(server);
//
// // setup mock response
// beforeEach(() => {
// server.use(
// rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({}))),
// );
// });
//
// it('should render', async () => {
// await renderInTestApp(<ExampleComponent />);
// expect(screen.getByText('Welcome to workflows!')).toBeInTheDocument();
// });
// });

View file

@ -0,0 +1,39 @@
import React from 'react';
import {Grid} from '@material-ui/core';
import {
Page,
Content,
} from '@backstage/core-components';
import {FetchTFState, ManageBlueprint} from "./FetchTFState";
export const BlueprintsComponent = () => (
<Page themeId="tool">
<Content>
{/* <ContentHeader title="Blueprint information">*/}
{/* <SupportButton>A description of your plugin goes here.</SupportButton>*/}
{/* </ContentHeader>*/}
<Grid container spacing={3} direction="column">
<Grid item>
<ManageBlueprint />
{/* <InfoCard title="Blueprint management">*/}
{/* <Typography color="textSecondary">*/}
{/* Manage this blueprint deployment*/}
{/* </Typography>*/}
{/* <IconButton aria-label="delete" size="medium">*/}
{/* <DeleteIcon />*/}
{/* </IconButton>*/}
{/* <IconButton aria-label="clear" size="medium">*/}
{/* <ClearIcon />*/}
{/* </IconButton>*/}
{/* <IconButton aria-label="link" size="medium">*/}
{/* <LinkOffRounded />*/}
{/* </IconButton>*/}
{/* </InfoCard>*/}
</Grid>
<Grid item>
<FetchTFState />
</Grid>
</Grid>
</Content>
</Page>
);

View file

@ -0,0 +1,353 @@
import React, {useState} from 'react';
import {Table, TableColumn, Progress, InfoCard, LinkButton} from '@backstage/core-components';
import Alert from '@material-ui/lab/Alert';
import useAsync from 'react-use/lib/useAsync';
import {
DiscoveryApi,
discoveryApiRef, OpenIdConnectApi,
useApi
} from '@backstage/core-plugin-api';
// eslint-disable-next-line no-restricted-imports
import {gunzipSync} from "zlib";
import {useEntity} from "@backstage/plugin-catalog-react"
import {
Dialog, DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
IconButton,
Typography
} from "@material-ui/core";
import DeleteIcon from "@material-ui/icons/Delete";
import ClearIcon from "@material-ui/icons/Clear";
import LinkOffRounded from "@material-ui/icons/LinkOffRounded";
import {keycloakOIDCAuthApiRef} from "../../plugin";
type TFState = {
terraform_version?: string
resources: {
name: string
provider: string
type: string
instances: {
attributes: {
arn: string
id: string
}
}[]
}[]
}
type Resource = {
name: string
provider: string
type: string
arn?: string
id?: string
}
type TFTableProps = {
resources: Resource[]
}
export const TFTable = (props: TFTableProps) => {
const columns: TableColumn[] = [
{ title: 'Name', field: 'name' },
{ title: 'Provider', field: 'provider' },
{ title: 'Type', field: 'type' },
{ title: 'Arn', field: 'arn' },
{ title: 'ID', field: 'id' },
];
return (
<Table
title="Resources provisioned by Terraform"
options={{ search: true, paging: false }}
columns={columns}
data={props.resources}
/>
);
}
export const FetchTFState = () => {
const apiRef = useApi(discoveryApiRef)
const entity = useEntity()
const oidcApi = useApi(keycloakOIDCAuthApiRef)
const secretName = `tfstate-default-${entity.entity.metadata.name}`
const { value, loading, error } = useAsync((): Promise<TFState> => {
return getTFState(secretName, "admin", apiRef, oidcApi)
})
if (loading) {
return <Progress />
} else if (error) {
return <Alert severity="error">{error}</Alert>;
}
// const a = await getTFState("tfstate-default-helloworld", "flux-system", discoveryApi)
// const tfdata = tfstate as TFState
const resources = value!.resources.map(val => {
const out: Resource = {
name: val.name,
provider: val.provider,
type: val.type,
}
if (val.instances.length > 0) {
out.arn = val.instances[0].attributes.arn
out.id = val.instances[0].attributes.id
}
return out
})
return <TFTable resources={resources}/>
};
// horrible
type payload = {
kind: string
apiVersion: string
items?: {
metadata: {
labels: {
[key: string]: string
}
}
}[]
metadata: {
annotations?: {
[key: string]: string
}
}
type: string
data: {
tfstate: string
}
}
async function getTFState(name: string, namespace: string, apiRef: DiscoveryApi, oidcRef: OpenIdConnectApi): Promise<TFState> {
const token = await oidcRef.getIdToken()
const baseUrl = await apiRef.getBaseUrl("kubernetes")
const proxyUrl = `${baseUrl}/proxy`
return new Promise(async (resolve, reject) => {
const resp = await fetch(`${proxyUrl}/api/v1/namespaces/${namespace}/secrets/${name}`, {
method: 'GET',
headers: {
'X-Kubernetes-Cluster': "canoe-packaging",
'Authorization': `Bearer ${token}`,
},
});
if (resp.ok) {
const payload = await resp.json() as payload
const data = Buffer.from(payload.data.tfstate, 'base64')
let compression = "gzip"
if ( payload.metadata.annotations && "encoding" in payload.metadata.annotations) {
compression = payload.metadata.annotations.encoding
}
if (compression === "gzip") {
const a = gunzipSync(data).toString("utf-8")
resolve(JSON.parse(a) as TFState)
}
reject(`unknown compression method specified: ${compression}`)
} else {
if (resp.status === 404) {
resolve( {resources: []} as TFState)
}
reject(`Failed to retrieve terraform information: ${resp.status}: ${resp.statusText} `)
}
})
}
enum workflowStatus {
UNKNOWN,
DELETING = 0,
CREATING,
DELETED,
NOTFOUND,
FAILED
}
export const ManageBlueprint = () => {
const entity = useEntity()
const [open, setOpen] = useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const oidcApi = useApi(keycloakOIDCAuthApiRef)
const apiRef = useApi(discoveryApiRef)
const { value, loading, error } = useAsync((): Promise<workflowStatus> => {
return getWorkflow(entity.entity.metadata.name, "admin", apiRef, oidcApi)
})
const module = entity.entity.metadata.annotations!["blueprint-module"]
if (module === undefined) {
return <Alert severity="error">"could not find blueprint module"</Alert>;
}
const handleConfirm = async (): Promise<void> => {
const ok = await createWorkflow(entity.entity.metadata.name, module, "admin", apiRef, oidcApi)
if (ok) {
handleClose()
} else {
console.log("oh no")
}
}
if (loading) {
return <Progress />
} else if (error) {
return <Alert severity="error">{error}</Alert>;
}
let text: string
switch (value) {
case workflowStatus.DELETING:
text = "This blueprint deployment is being deleted"
break
case workflowStatus.DELETED:
text = "This blueprint deployment was successfully deleted"
break
case workflowStatus.NOTFOUND:
text = "Manage this blueprint with the buttons below"
break
case workflowStatus.FAILED:
return <Alert severity="error">"failed to delete blueprint deployment"</Alert>;
default:
return <Alert severity="error">"could not determine blueprint status"</Alert>;
}
return (
<InfoCard title="Blueprint management">
<Typography color="textSecondary">
{text}
</Typography>
<IconButton aria-label="delete" size="medium" onClick={handleClickOpen}>
<DeleteIcon />
</IconButton>
<Dialog open={open} onClose={handleClose}>
<DialogTitle style={{ cursor: 'move' }} id="title">
Confirmation
</DialogTitle>
<DialogContent>
<DialogContentText>
Are you sure you want to delete this?
</DialogContentText>
</DialogContent>
<DialogActions>
<LinkButton onClick={handleClose} to="cba">
Cancel
</LinkButton>
<LinkButton onClick={handleConfirm}
to="abc" color="primary">Delete</LinkButton>
</DialogActions>
</Dialog>
<IconButton aria-label="clear" size="medium">
<ClearIcon />
</IconButton>
<IconButton aria-label="link" size="medium">
<LinkOffRounded />
</IconButton>
</InfoCard>
)
}
async function getWorkflow(entityId: string, namespace: string, apiRef: DiscoveryApi, oidcRef: OpenIdConnectApi ): Promise<workflowStatus> {
const token = await oidcRef.getIdToken()
const baseUrl = await apiRef.getBaseUrl("kubernetes")
const proxyUrl = `${baseUrl}/proxy`
return new Promise(async (resolve, reject) => {
const queryParams = new URLSearchParams({
labelSelector: `entity-id=${entityId},workflow-kind=delete`,
limit: "1"
}).toString()
const resp = await fetch(`${proxyUrl}/apis/argoproj.io/v1alpha1/namespaces/${namespace}/workflows?${queryParams}`, {
method: 'GET',
headers: {
'X-Kubernetes-Cluster': "canoe-packaging",
Authorization: `Bearer ${token}`,
},
});
if (resp.ok) {
const payload = await resp.json() as payload
if (payload.items!.length > 0) {
const labels = payload.items![0].metadata.labels
if ("workflows.argoproj.io/phase" in labels) {
switch (labels["workflows.argoproj.io/phase"]) {
case "Running":
resolve(workflowStatus.DELETING)
break;
case "Succeeded":
resolve(workflowStatus.DELETED)
break
case "Failed":
resolve(workflowStatus.FAILED)
break
default:
reject(workflowStatus.UNKNOWN)
break
}
}
} else {
resolve(workflowStatus.NOTFOUND)
}
} else {
reject(`Failed to retrieve terraform information: ${resp.status}: ${resp.statusText} `)
}
})
}
async function createWorkflow(entityId: string, blueprintName: string, namespace: string, apiRef: DiscoveryApi, oidcRef: OpenIdConnectApi): Promise<Boolean> {
const token = await oidcRef.getIdToken()
const baseUrl = await apiRef.getBaseUrl("kubernetes")
const proxyUrl = `${baseUrl}/proxy`
return new Promise(async (resolve, reject) => {
const queryParams = new URLSearchParams({
fieldValidation: "Strict",
}).toString()
const body = {
"apiVersion": "argoproj.io/v1alpha1",
"kind": "Workflow",
"metadata": {
"generateName": "blue-prints-delete-",
"namespace": "admin",
},
"spec": {
"arguments": {
"parameters": [
{
"name": "blueprint-name",
"value": `${blueprintName}`
},
{
"name": "entityId",
"value": `${entityId}`
}
]
},
"workflowTemplateRef": {
"name": "blueprints-delete"
}
}
}
const resp = await fetch(`${proxyUrl}/apis/argoproj.io/v1alpha1/namespaces/${namespace}/workflows?${queryParams}`, {
method: 'POST',
headers: {
'X-Kubernetes-Cluster': "canoe-packaging",
Authorization: `Bearer ${token}`,
'Content-Type': "application/json"
},
body: JSON.stringify(body)
});
if (resp.ok) {
resolve(true)
} else {
reject(`Failed to delete blueprints deployment: ${resp.status}: ${resp.statusText} `)
}
})
}

View file

@ -0,0 +1 @@
export { BlueprintsComponent } from './ExampleComponent';

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,2 @@
export { workflowsPlugin, EntityWorkflowsContent, keycloakOIDCAuthApiRef } from './plugin';

View file

@ -0,0 +1,7 @@
import { workflowsPlugin } from './plugin';
describe('workflows', () => {
it('should export plugin', () => {
expect(workflowsPlugin).toBeDefined();
});
});

View file

@ -0,0 +1,31 @@
import {
ApiRef, BackstageIdentityApi, createApiRef,
createPlugin,
createRoutableExtension,
OpenIdConnectApi,
ProfileInfoApi, SessionApi
} from '@backstage/core-plugin-api';
import { rootCatalogWorkflowsRouteRef } from './routes';
export const workflowsPlugin = createPlugin({
id: 'workflows',
routes: {
entityContent: rootCatalogWorkflowsRouteRef,
},
});
export const EntityWorkflowsContent = workflowsPlugin.provide(
createRoutableExtension({
name: 'EntityWorkflowsContent',
component: () =>
import('./components/BlueprintComponent').then(m => m.BlueprintsComponent),
mountPoint: rootCatalogWorkflowsRouteRef,
}),
);
export const keycloakOIDCAuthApiRef: ApiRef<
OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi
> = createApiRef({
id: 'auth.keycloak-oidc-provider',
});

View file

@ -0,0 +1,5 @@
import { createRouteRef } from '@backstage/core-plugin-api';
export const rootCatalogWorkflowsRouteRef = createRouteRef({
id: 'workflows',
});

View file

@ -0,0 +1,2 @@
import '@testing-library/jest-dom';
import 'cross-fetch/polyfill';

80
test-template.yaml Normal file
View file

@ -0,0 +1,80 @@
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: test-template
title: TESTING
description: test
spec:
owner: backstage/techdocs-core
type: service
# these are the steps which are rendered in the frontend with the form input
parameters:
- title: Fill in some steps
required:
- name
- owner
properties:
name:
title: Application Name
type: string
description: Unique name of the component
ui:autofocus: true
ui:options:
rows: 5
owner:
title: Owner
type: string
description: Owner of the component
ui:field: OwnerPicker
ui:options:
catalogFilter:
kind: Group
labels:
title: Labels
type: object
additionalProperties:
type: string
description: Labels to apply to the application
namespace:
title: Namespace
type: string
description: Namespace to deploy this application into. Optional. Defaults to application name.
ui:options:
rows: 5
clusterName:
title: Cluster Name
type: string
default: canoe-packaging
description: Name of the cluster to run this in
- title: Workflow params
properties:
workflowParams:
title: workflow parameters
type: array
description: workflow parameters
ui:autofocus: true
items:
type: object
required:
- name
- value
properties:
name:
type: string
value:
type: string
steps:
- id: flow
name: Flow
action: workflows:argo:invoke
input:
templateName: workflow-template-whalesay-template
namespace: admin
clusterName: ${{ parameters.clusterName }}
parameters: ${{ parameters.workflowParams }}
# output:
# links:
# - title: Open in catalog
# icon: catalog
# entityRef: ${{ steps['register'].output.entityRef }}

View file

@ -1,7 +1,10 @@
{
"extends": "@backstage/cli/config/tsconfig.json",
"include": [
"packages/*/src"
"packages/*/src",
"plugins/*/src",
"plugins/*/dev",
"plugins/*/migrations"
],
"exclude": ["node_modules"],
"compilerOptions": {

38521
yarn.lock

File diff suppressed because it is too large Load diff