Merge remote-tracking branch 'upstream/master' into master

This commit is contained in:
Nir Gazit
2021-01-09 21:59:29 +02:00
335 changed files with 9216 additions and 2519 deletions
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core-api': patch
---
Fix for GitHub and SAML auth not properly updating session state when already logged in.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
GitLab preparer uses the right token (primarily the same one as the publisher, falling back to the integrations token)
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/create-app': minor
---
Add search plugin to default template for CLI created apps
-7
View File
@@ -1,7 +0,0 @@
---
'@backstage/core': patch
'@backstage/test-utils': patch
'@backstage/plugin-graphiql': patch
---
Update to use new plugin extension API
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core-api': patch
---
Introduce new plugin extension API
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-backend': patch
---
Added configuration schema for the commonly used properties
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Clearer error message when preparer or publisher type can't be determined.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': patch
---
Bumping the version for `create-app` so that we can use the latest versions of internal packages and rebuild the version which is passed to the package.json
+6 -5
View File
@@ -36,6 +36,7 @@ Codecov
codehilite
Codehilite
codeowners
composability
composable
config
Config
@@ -49,6 +50,7 @@ dataflow
deadnaming
destructured
dev
devops
devs
dhenneke
discoverability
@@ -62,7 +64,6 @@ Docusaurus
Dominik
dtuite
dzolotusky
eg
Ek
env
Env
@@ -134,12 +135,14 @@ neuro
newrelic
nginx
Niklas
nodegit
nohoist
nonces
npm
nvarchar
nvm
oauth
Oauth
OAuth
oidc
Okta
Oldsberg
@@ -158,7 +161,6 @@ prebaked
preconfigured
prepack
Preprarer
Prerequisities
productional
Protobuf
proxying
@@ -192,7 +194,6 @@ semlas
semver
Serverless
Sinon
smartsymobls
Snyk
sourcemaps
sparklines
@@ -219,7 +220,6 @@ Templater
templaters
Templaters
Thauer
theres
toc
tolerations
Tolerations
@@ -237,6 +237,7 @@ upvote
url
utils
validators
varchar
Voi
Wealthsimple
Weaveworks
+5 -5
View File
@@ -11,6 +11,11 @@ on:
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#scanning-pull-requests
paths-ignore:
- '**/*.md'
- '**/*.yml'
- '**/*.yaml'
schedule:
- cron: '0 8 * * 6'
@@ -36,11 +41,6 @@ jobs:
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
+6 -3
View File
@@ -51,10 +51,13 @@ jobs:
- name: test
run: yarn lerna -- run test
# credit: https://github.com/appleboy/discord-action/issues/3#issuecomment-731426861
- name: Discord notification
if: ${{ failure() }}
uses: Ilshidur/action-discord@0.2.0
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
with:
args: 'Windows master build failed https://github.com/{{GITHUB_REPOSITORY}}/actions/runs/{{GITHUB_RUN_ID}}'
run: |
$MESSAGE=@"
{\"content\": \"Windows master build failed https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}\"}
"@
C:\msys64\usr\bin\curl.exe -i -H "Accept: application/json" -H "Content-Type:application/json" -X POST $env:DISCORD_WEBHOOK --data $MESSAGE
+82 -15
View File
@@ -70,21 +70,6 @@ jobs:
bash <(curl -s https://codecov.io/bash) -f packages/core/coverage/* -F core
bash <(curl -s https://codecov.io/bash) -f packages/core-api/coverage/* -F core-api
# Publishes current version of packages that are not already present in the registry
- name: publish
if: matrix.node-version == '12.x'
run: yarn lerna -- publish from-package --yes
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# Tags the commit with the version in the core package if the tag doesn't exist
- uses: Klemensas/action-autotag@1.2.3
if: matrix.node-version == '12.x'
with:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
package_root: 'packages/core'
tag_prefix: 'v'
- name: Discord notification
if: ${{ failure() }}
uses: Ilshidur/action-discord@0.2.0
@@ -92,3 +77,85 @@ jobs:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
with:
args: 'Master build failed https://github.com/{{GITHUB_REPOSITORY}}/actions/runs/{{GITHUB_RUN_ID}}'
# A separate release build that is only run for commits that are the result of merging the "Version Packages" PR
# We can't re-use the output from the above step, but we'll have a guaranteed node_modules cache and
# only run the build steps that are necessary for publishing
release:
if: contains(github.event.commits.*.author.username, 'backstage-service') && contains(github.event.head_commit.message, 'from backstage/changeset-release/master')
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
env:
CI: 'true'
NODE_OPTIONS: --max-old-space-size=4096
steps:
- uses: actions/checkout@v2
# Beginning of yarn setup, keep in sync between all workflows, see ci.yml
- name: use node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org/ # Needed for auth
- name: cache all node_modules
id: cache-modules
uses: actions/cache@v2
with:
path: '**/node_modules'
key: ${{ runner.os }}-v${{ matrix.node-version }}-node_modules-${{ hashFiles('yarn.lock', '**/package.json') }}
- name: find location of global yarn cache
id: yarn-cache
if: steps.cache-modules.outputs.cache-hit != 'true'
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: cache global yarn cache
uses: actions/cache@v2
if: steps.cache-modules.outputs.cache-hit != 'true'
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: yarn install
run: yarn install --frozen-lockfile
# End of yarn setup
- name: build type declarations
run: yarn tsc:full
- name: build packages
run: yarn lerna -- run --ignore example-app build
# Publishes current version of packages that are not already present in the registry
- name: publish
run: yarn lerna -- publish from-package --yes
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# Creates the next available tag with format "release-<year>-<month>-<day>[.<n>]"
- name: Create a release tag
id: create_tag
run: node scripts/create-release-tag.js
env:
GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
# Convert the newly created tag into a release with changelog information
- name: Create release on GitHub
run: node scripts/create-github-release.js ${{ steps.create_tag.outputs.tag_name }} 1
env:
GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
# Notify everyone about this great new release :D
- name: Discord notification
uses: Ilshidur/action-discord@0.2.0
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
TAG_NAME: ${{ steps.create_tag.outputs.tag_name }}
with:
args: 'A new release has been published! https://github.com/backstage/backstage/releases/tag/{{TAG_NAME}}'
+1
View File
@@ -55,6 +55,7 @@ jobs:
- name: tsc
run: yarn tsc
- name: build
run: yarn build
+1
View File
@@ -16,3 +16,4 @@
| [Telenor Sweden](https://www.telenor.se) | [@O5ten](https://github.com/O5ten) | Building a developer portal for scaffolding projects towards our unified build environment and microservice stacks |
| [Fiverr](https://www.fiverr.com) | [@nirga](https://github.com/nirga) | Unifying separate tools that developers are using today (i.e. monitoring, dead letter queues management, etc.) into a single platform. |
| [Zalando SE](https://www.zalando.de) | [@leviferreira](https://github.com/leviferreira) | Building V2 of the Internal Development Portal. |
| [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. |
+1 -1
View File
@@ -18,7 +18,7 @@ Harassment includes, but is not limited to:
- Offensive comments related to gender, gender identity and expression, sexual orientation, disability, mental illness, neuro(a)typicality, physical appearance, body size, race, age, regional discrimination, political or religious affiliation
- Unwelcome comments regarding a persons lifestyle choices and practices, including those related to food, health, parenting, drugs, and employment
- Deliberate misgendering. This includes deadnaming or persistently using a pronoun that does not correctly reflect a person's gender identity. You must address people by the name they give you when not addressing them by their username or handle
- Physical contact and simulated physical contact (eg, textual descriptions like “_hug_” or “_backrub_”) without consent or after a request to stop
- Physical contact and simulated physical contact (e.g., textual descriptions like “_hug_” or “_backrub_”) without consent or after a request to stop
- Threats of violence, both physical and psychological
- Incitement of violence towards any individual, including encouraging a person to commit suicide or to engage in self-harm
- Deliberate intimidation
+33 -21
View File
@@ -6,19 +6,19 @@ Therefore we want to create strong community of contributors -- all working toge
Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. ❤️
Backstage is released under the Apache2.0 License, and original creations contributed to this repo are accepted under the same license.
Backstage is released under the Apache 2.0 License, and original creations contributed to this repo are accepted under the same license.
# Types of Contributions
## Types of Contributions
## Report bugs
### Report bugs
No one likes bugs. Report bugs as an issue [here](https://github.com/backstage/backstage/issues/new?template=bug_template.md).
## Fix bugs or build new features
### Fix bugs or build new features
Look through the GitHub issues for [bugs](https://github.com/backstage/backstage/labels/bugs), [good first issues](https://github.com/backstage/backstage/labels/good%20first%20issue) or [help wanted](https://github.com/backstage/backstage/labels/help%20wanted).
## Build a plugin
### Build a plugin
The value of Backstage grows with every new plugin that gets added. Wouldn't it be fantastic if there was a plugin for every infrastructure project out there? We think so. And we would love your help.
@@ -26,27 +26,27 @@ A great reference example of a plugin can be found on [our blog](https://backsta
What kind of plugins should/could be created? Some inspiration from the 120+ plugins that we have developed inside Spotify can be found [here](https://backstage.io/demos), but we will keep a running list of suggestions labeled with [[plugin]](https://github.com/backstage/backstage/labels/plugin).
## Suggesting a plugin
### Suggesting a plugin
If you start developing a plugin that you aim to release as open source, we suggest that you create a [new Issue](https://github.com/backstage/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME). This helps the community know what plugins are in development.
You can also use this process if you have an idea for a good plugin but you hope that someone else will pick up the work.
## Adding Non-code Contributions
### Adding Non-code Contributions
Since there is such a large landscape of possible development, build, and deployment environments, we welcome community contributions in these areas in the [`/contrib`](https://github.com/backstage/backstage/tree/master/contrib) folder of the project. This is an excellent place to put things that help out the community at large, but which may not fit within the scope of the core product to support natively. Here, you will find Helm charts, alternative Docker images, and much more.
## Write Documentation
### Write Documentation
The current documentation is very limited. Help us make the `/docs` folder come alive.
## Contribute to Storybook
### Contribute to Storybook
We think the best way to ensure different plugins provide a consistent experience is through a solid set of reusable UI/UX components. Backstage uses [Storybook](http://backstage.io/storybook).
Either help us [create new components](https://github.com/backstage/backstage/labels/help%20wanted) or improve stories for the existing ones (look for files with `*.stories.tsx`).
## Submit Feedback
### Submit Feedback
The best way to send feedback is to file [an issue](https://github.com/backstage/backstage/issues).
@@ -58,47 +58,59 @@ If you are proposing a feature:
- Remember that this is a volunteer-driven project, and that contributions
are welcome :)
## Add your company to ADOPTERS
### Add your company to ADOPTERS
Have you started using Backstage? Adding your company to [ADOPTERS](ADOPTERS.md) really helps the project.
# Get Started!
## Get Started!
So...feel ready to jump in? Let's do this. 👏🏻💯
Start by reading our [Getting Started](https://backstage.io/docs/getting-started/) page. If you need help, just jump into our [Discord chatroom](https://discord.gg/MUpMjP2).
# Coding Guidelines
## Coding Guidelines
All code is formatted with `prettier` using the configuration in the repo. If possible we recommend configuring your editor to format automatically, but you can also use the `yarn prettier --write <file>` command to format files.
A consistent coding style is included via [EditorConfig](https://editorconfig.org/) with the file [`.editorconfig`](.editorconfig) at the root of the repo. Depending on your editor of choice, it will either support it out of the box or you can [download a plugin](https://editorconfig.org/#download) for the config to be applied.
If you're contributing to the backend or CLI tooling, be mindful of cross-platform support. [This](https://shapeshed.com/writing-cross-platform-node/) blog post is a good guide of what to keep in mind when writing cross-platform NodeJS.
Also be sure to skim through our [ADRs](https://github.com/backstage/backstage/tree/master/docs/architecture-decisions) to see if they cover what you're working on. In particular [ADR006: Avoid React.FC and React.SFC](https://github.com/backstage/backstage/blob/master/docs/architecture-decisions/adr006-avoid-react-fc.md) is one to look out for.
If there are any updates in `markdown` file please make sure to run `yarn run lint:docs`. Though it is checked on `lint-staged`. It is required to install [vale](https://docs.errata.ai/vale/install) separately and make sure it is accessed by global command.
# Creating Changesets
## Creating Changesets
We use [changesets](https://github.com/atlassian/changesets) to help us prepare releases. It helps us make sure that every package affected by a change gets a proper version number and an entry in its `CHANGELOG.md`. To make the process of generating releases easy. it helps when contributors include changesets with their pull requests.
We use [changesets](https://github.com/atlassian/changesets) to help us prepare releases. They help us make sure that every package affected by a change gets a proper version number and an entry in its `CHANGELOG.md`. To make the process of generating releases easy, it helps when contributors include changesets with their pull requests.
## To create a changeset
### When to use a changeset?
Any time a patch, minor, or major change aligning to [Semantic Versioning](https://semver.org) is made to any published package in `packages/` or `plugins/`, a changeset should be used. It helps to align your change to the [Backstage stability index](https://backstage.io/docs/overview/stability-index) for the package you are changing, for example, when to provide additional clarity on deprecation or impacting changes which will then be included into CHANGELOGs.
In general, changesets are not needed for the documentation, build utilities, contributed samples in `contrib/`, or the [example `packages/app`](packages/app).
### How to create a changeset
1. Run `yarn changeset`
2. Select which packages you want to include a changeset for
3. Select impact of change that you're introducing (minor, major or patch)
3. Select impact of change that you're introducing (patch, minor, or major)
4. Add generated changeset to Git
5. Push the commit with your changeset to the branch associated with your PR
6. Accept our gratitude for making the release process easier on the maintainer
6. Accept our gratitude for making the release process easier on the maintainers
For more information, checkout [adding a changeset](https://github.com/atlassian/changesets/blob/master/docs/adding-a-changeset.md) documentation in changesets repository.
For more information, checkout [adding a changeset](https://github.com/atlassian/changesets/blob/master/docs/adding-a-changeset.md) documentation in the changesets repository.
# Code of Conduct
## Merging to Master
For those contributors who have earned write access to the repository, when a pull request is approved, in general we prefer the author of the PR to perform the merge themselves. This allows them to own accountability for the change and they likely know best how or when to address pending fixes or additional follow-ups. In this way, we all help contribute to the project's successful outcomes.
## Code of Conduct
This project adheres to the [Spotify FOSS Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code.
[code-of-conduct]: https://github.com/backstage/backstage/blob/master/CODE_OF_CONDUCT.md
# Security Issues?
## Security Issues?
See [SECURITY](SECURITY.md).
+1 -5
View File
@@ -10,13 +10,9 @@
[![Codecov](https://img.shields.io/codecov/c/github/backstage/backstage)](https://codecov.io/gh/backstage/backstage)
[![](https://img.shields.io/npm/v/@backstage/core?label=Version)](https://github.com/backstage/backstage/releases)
> We recently moved from `spotify/backstage`, update your remotes!
>
> `git remote set-url origin git@github.com:backstage/backstage.git`
## What is Backstage?
[Backstage](https://backstage.io/) is an open platform for building developer portals. Powered by a centralized service catalog, Backstage restores order to your microservices and infrastructure. So your product teams can ship high-quality code quickly — without compromising autonomy.
[Backstage](https://backstage.io/) is an open platform for building developer portals. Powered by a centralized service catalog, Backstage restores order to your microservices and infrastructure and enables your product teams to ship high-quality code quickly — without compromising autonomy.
Backstage unifies all your infrastructure tooling, services, and documentation to create a streamlined development environment from end to end.
+8 -1
View File
@@ -76,7 +76,7 @@ techdocs:
generators:
techdocs: 'docker' # Alternatives - 'local'
publisher:
type: 'local' # Alternatives - 'googleGcs'. Read documentation for using alternatives.
type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives.
sentry:
organization: my-company
@@ -206,6 +206,13 @@ scaffolder:
api:
token:
$env: AZURE_TOKEN
bitbucket:
api:
host: https://bitbucket.org
username:
$env: BITBUCKET_USERNAME
token:
$env: BITBUCKET_TOKEN
auth:
environment: development
### Providing an auth.session.secret will enable session support in the auth-backend
Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 101 KiB

+144
View File
@@ -0,0 +1,144 @@
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="605px" height="362px" viewBox="-0.5 -0.5 605 362" content="&lt;mxfile host=&quot;046c5cfb-a3a2-464c-a595-f674d31136ed&quot; modified=&quot;2020-12-26T01:33:35.255Z&quot; agent=&quot;5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.52.1 Chrome/83.0.4103.122 Electron/9.3.5 Safari/537.36&quot; etag=&quot;nIR1gVsObWk5MHRRKeB7&quot; version=&quot;13.10.0&quot; type=&quot;embed&quot;&gt;&lt;diagram id=&quot;fZcCmqQ9Q5JzR6r55YHy&quot; name=&quot;Page-1&quot;&gt;7VhNc5swEP01HJtBiA/7iI3dHtqT20lzlIUCajCishzb/fVdQBhkSOLWjtPJ9JJo367Qat/TImzh6Wr3UZIi/SJillmOHe8sHFmO43sI/pbAXgPBuAYSyeMasltgwX+xGkQNuuExW2ushpQQmeKFCVKR54wqAyNSiq0Zdi+y2AAKkrAesKAk66O3PFapRn3bbh2fGE9SvfS4cSwJfUik2OR6PcvBczQPZ7PavSLNs3T8OiWx2HYgPLPwVAqh6tFqN2VZWVqzbPMnvIe8JcvVKRM8nccjyTasSblKTO2bYmxTrtiiILS0t8C3hSepWmVgIRje8yybikxIsHORQ9BkraR4YA0INXC9YDKOwKNXY1Kx3ZMpo0MhQF9MrJiSewjRE5xRPUNLy3Frc9vy5Nu4xtIORdjXWyVaG8nhyW19YKBLNFwu1+9VR0iVikTkJPssRKFr8oMptdeyJhslzIqxHVffYWzr8V0Hj3YdR7RvjBwSrabceI151/W10yqrnReH5XFoqQFkzsv9Vf6aqEbi6FnqKlWzWM+s68Di3jk6ohFqJTaS6ijNiyIyYTrK9YbZliwjij+ajz+Lu6DHXZn+Qpu6PuYuzyAX/SPkDlHZp/1ydLpXohMPNC4/U2VDEpByl2f/50Y0jg/rirgQApBf7FonjBL9v3rKsgHCeMXzBoWslm2k/W3NZMdVr9x4j8RmKuuFprp+YIqmLxLZ7b2Ah6MojKZ6px0cRdjzgkNSA6z2OvLT7ddFRv9Fdr8Bo8DtN+Cxd4Ez7FyN9dsFBIeUAmtqmP3TaIeyqiNuDTr10R14i5KMJzmYFMgCmeFJSRKHS0qoHSsex+Uyg2Iy5Wbo6RXVgbEhDuz3tYFQXxvOJd7N7rW08ZXRNBJ0/a6awll3Mhd5R5eyAeJfrSl41yJ+Aeq2JxsKPJzTEt4J6X7whpyPXuKcHjbd0lqWazmOu9AJ2nCHtNE95EvZoIXION3//atBMliXLKuAku/yellnUrlPfyMUgueqqq83sbxjgQx+pg1egC/RG4Lxjdkd0Aj3lNKIx/hkcy8glPEbC8VsF/+l8pxUkBOcIBVv6Ov+z6UCZvs7S+Xr/JaFZ78B&lt;/diagram&gt;&lt;/mxfile&gt;" style="background-color: rgb(241, 250, 238);">
<defs/>
<g>
<rect x="0" y="0" width="603" height="360" fill="none" stroke="#457b9d" pointer-events="all"/>
<path d="M 213 175 L 126 222" fill="none" stroke="#457b9d" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 387 175 L 474 222" fill="none" stroke="#457b9d" stroke-miterlimit="10" pointer-events="stroke"/>
<rect x="213" y="80" width="174" height="95" fill="#a8dadc" stroke="#457b9d" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 172px; height: 1px; padding-top: 128px; margin-left: 214px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #1D3557; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
<font style="font-size: 16px">
<b>
Admin
</b>
User
</font>
</div>
</div>
</div>
</foreignObject>
<text x="300" y="131" fill="#1D3557" font-family="Helvetica" font-size="12px" text-anchor="middle">
Admin User
</text>
</switch>
</g>
<rect x="5" y="12" width="111" height="20" fill="none" stroke="none" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 109px; height: 1px; padding-top: 22px; margin-left: 6px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #1D3557; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
<font style="font-size: 16px">
<b>
AWS Account
</b>
</font>
</div>
</div>
</div>
</foreignObject>
<text x="61" y="26" fill="#1D3557" font-family="Helvetica" font-size="12px" text-anchor="middle">
AWS Account
</text>
</switch>
</g>
<rect x="387" y="222" width="174" height="95" fill="#a8dadc" stroke="#457b9d" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 172px; height: 1px; padding-top: 270px; margin-left: 388px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #1D3557; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
<font style="font-size: 16px">
<b>
TechDocs
</b>
User
</font>
</div>
</div>
</div>
</foreignObject>
<text x="474" y="273" fill="#1D3557" font-family="Helvetica" font-size="12px" text-anchor="middle">
TechDocs User
</text>
</switch>
</g>
<rect x="39" y="222" width="174" height="95" fill="#a8dadc" stroke="#457b9d" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 172px; height: 1px; padding-top: 270px; margin-left: 40px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #1D3557; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
<font style="font-size: 16px">
<b>
S3 Bucket
</b>
</font>
</div>
</div>
</div>
</foreignObject>
<text x="126" y="273" fill="#1D3557" font-family="Helvetica" font-size="12px" text-anchor="middle">
S3 Bucket
</text>
</switch>
</g>
<rect x="451.5" y="159" width="46" height="34" fill="none" stroke="none" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 176px; margin-left: 475px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
<font color="#457b9d" style="font-size: 14px">
User
<br/>
policy
</font>
</div>
</div>
</div>
</foreignObject>
<text x="475" y="180" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
User...
</text>
</switch>
</g>
<rect x="99.5" y="159" width="53" height="34" fill="none" stroke="none" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 176px; margin-left: 126px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
<font color="#457b9d" style="font-size: 14px">
Bucket
<br/>
policy
</font>
</div>
</div>
</div>
</foreignObject>
<text x="126" y="180" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
Bucket...
</text>
</switch>
</g>
</g>
<switch>
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
<a transform="translate(0,-5)" xlink:href="https://desk.draw.io/support/solutions/articles/16000042487" target="_blank">
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
Viewer does not support full SVG 1.1
</text>
</a>
</switch>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

+18
View File
@@ -61,6 +61,19 @@ If your authentication provider is any of the above mentioned providers, you can
configure them by setting the right variables in `app-config.yaml` under the
`auth` section.
### SAML
The SAML Provider is currently under development. Additional validation and
profile handling is still required before use in production.
To configure the SAML Auth provider, look at the configuration parameters
supported by
[Passport-SAML](https://github.com/node-saml/passport-saml#config-parameter-details)
under the `auth.providers.saml` key
For security reasons, validate that the response from the IdP is indeed signed
by also providing the `cert` configuration.
### Configuration
Each authentication provider (except SAML) needs five parameters: an OAuth
@@ -96,6 +109,11 @@ auth:
development:
clientId:
$env:
saml:
entryPoint:
$env: AUTH_SAML_ENTRY_POINT
issuer:
$env: AUTH_SAML_ISSUER
...
```
+48 -1
View File
@@ -28,6 +28,7 @@ app:diff Diff an existing app with the creation template
app:serve Serve an app for local development
backend:build Build a backend plugin
backend:bundle Bundle the backend into a deployment archive
backend:build-image Bundles the package into a docker image
backend:dev Start local development server with HMR for the backend
@@ -166,7 +167,7 @@ Options:
## backend:build
Scope: `backend`, `backend-plugin`
Scope: `backend-plugin`
This builds a backend package for publishing and use in production. The build
output is written to `dist/`. Be sure to list any additional file that the
@@ -180,6 +181,52 @@ Options:
-h, --help display help for command
```
## backend:bundle
Scope: `backend`
Bundle the backend and all of its local dependencies into a deployment archive.
The archive is written to `dist/bundle.tar.gz`, and contains the packaged
version of all dependencies of the target package, along with the target package
itself. The layout of the packages in the archive is the same as the directory
layout in the target monorepo, and the bundle also contains the root
`package.json` and `yarn.lock`.
To use the bundle, extract it into a target directory, run
`yarn install --production`, and then start the target backend package using for
example `node package/backend`.
The `dist/bundle.tar.gz` is accompanied by a `dist/skeleton.tar.gz`, which has
the same layout, but only contains `package.json` files and `yarn.lock`. This
can be used to run a `yarn install` in environments that will benefit from the
caching that this enables, such as Docker image builds. To use the skeleton
archive, simply extract it first, run install, and then extract the main bundle.
The following is an example of a `Dockerfile` that can be used to package the
output of `backstage-cli backend:bundle` into an image:
```Dockerfile
FROM node:14-buster
WORKDIR /app
ADD yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./
RUN yarn install --production --network-timeout 600000 && rm -rf "$(yarn cache dir)"
ADD packages/backend/dist/bundle.tar.gz app-config.yaml ./
CMD node packages/backend
```
```text
Usage: backstage-cli backend:bundle [options]
Bundle the backend into a deployment archive
Options:
--build-dependencies Build all local package dependencies before bundling the backend
-h, --help display help for command
```
## backend:build-image
Scope: `backend`
+4
View File
@@ -36,6 +36,10 @@ export interface Config {
* @visibility frontend
*/
baseUrl: string;
// Use @items.<name> to assign annotations to primitive array items
/** @items.visibility frontend */
myItems: string[];
};
}
```
+8 -7
View File
@@ -141,16 +141,17 @@ itself:
$file: ./my-secret.txt
```
### Data File Secrets
### Including Files
This reads secrets from a path within a JSON-like data file. The file path
behaves similar to file secrets, but with the addition of a url fragment that is
used to point to a specific value inside the file. Supported file extensions are
`.json`, `.yaml`, and `.yml`. For example, the following would read out
`my-secret-key` from `my-secrets.json`:
The `$include` keyword can be used to load in JSON data from an external file.
It's able to load and parse data from `.json`, `.yml`, and `.yaml` files. It's
also possible to include a url fragment (`#`) to point to a value at the given
path in the file.
For example, the following would read `my-secret-key` from `my-secrets.json`:
```yaml
$data: ./my-secrets.json#deployment.key
$include: ./my-secrets.json#deployment.key
```
Example `my-secrets.json` file:
@@ -44,8 +44,8 @@ Backstage model and the primary way to discover existing functionality in the
ecosystem.
APIs are implemented by components and form boundaries between components. They
might be defined using an RPC IDL (eg Protobuf, GraphQL, ...), a data schema (eg
Avro, TFRecord, ...), or as code interfaces. In any case, APIs exposed by
might be defined using an RPC IDL (e.g., Protobuf, GraphQL, ...), a data schema
(e.g., Avro, TFRecord, ...), or as code interfaces. In any case, APIs exposed by
components need to be in a known machine-readable format so we can build further
tooling and analysis on top.
+6 -8
View File
@@ -45,8 +45,6 @@ about TechDocs and the philosophy in its
[v2]: https://github.com/backstage/backstage/milestone/22
[v3]: https://github.com/backstage/backstage/milestone/17
<!-- TODO: Add link to milestone for v3 -->
## Use Cases
#### TechDocs V.0
@@ -110,12 +108,12 @@ providers are used.
| GitLab | Yes ✅ |
| GitLab Enterprise | Yes ✅ |
| File Storage Provider | Support Status | Track status |
| --------------------------------- | -------------- | ----------------------------------------------------------- |
| Local Filesystem of Backstage app | Yes ✅ | |
| Google Cloud Storage (GCS) | Yes ✅ | |
| Amazon Web Services (AWS) S3 | No ❌ | [#3714](https://github.com/backstage/backstage/issues/3714) |
| Azure Storage | No ❌ | |
| File Storage Provider | Support Status |
| --------------------------------- | ----------------------------------------------------------------- |
| Local Filesystem of Backstage app | Yes ✅ |
| Google Cloud Storage (GCS) | Yes ✅ |
| Amazon Web Services (AWS) S3 | Yes ✅ |
| Azure Storage | No ❌ [#3938](https://github.com/backstage/backstage/issues/3938) |
[Reach out to us](#feedback) if you want to request more platforms.
+3 -3
View File
@@ -141,13 +141,13 @@ Status of all the features mentioned above.
**In place ✅**
- Basic setup with techdocs-backend file server as storage.
- Basic setup with cloud storage solution.
**Work in progress 🚧**
- Basic setup with cloud storage solution.
- `techdocs-cli` is able to generate docs in CI/CD environment.
- `techdocs-cli` is able to publish docs site to any storage.
**Not implemented yet ❌**
- `techdocs-cli` is able to generate docs in CI/CD environment.
- `techdocs-cli` is able to publish docs site to any storage.
- `techdocs-backend` integration with Backstage access control management.
+18 -10
View File
@@ -13,18 +13,15 @@ configuration options for TechDocs.
# File: app-config.yaml
techdocs:
# TechDocs makes API calls to techdocs-backend using this URL. e.g. get docs of an entity, get metadata, etc.
requestUrl: http://localhost:7000/api/techdocs
# Just another route in techdocs-backend where TechDocs requests the static files from. This URL uses an HTTP middleware
# to serve files from either a local directory or an External storage provider.
storageUrl: http://localhost:7000/api/techdocs/static/docs
# generators.techdocs can have two values: 'docker' or 'local'. This is to determine how to run the generator - whether to
# spin up the techdocs-container docker image or to run mkdocs locally (assuming all the dependencies are taken care of).
# You want to change this to 'local' if you are running Backstage using your own custom Docker setup and want to avoid running
@@ -34,7 +31,6 @@ techdocs:
generators:
techdocs: 'docker'
# techdocs.builder can be either 'local' or 'external.
# If builder is set to 'local' and you open a TechDocs page, techdocs-backend will try to generate the docs, publish to storage
# and show the generated docs afterwords. This is the "Basic" setup of the TechDocs Architecture.
@@ -44,30 +40,42 @@ techdocs:
builder: 'local'
# techdocs.publisher is used to configure the Storage option, whether you want to use the local filesystem to store generated docs
# or you want to use External storage providers like Google Cloud Storage, AWS S3, etc.
publisher:
# techdocs.publisher.type can be - 'local' or 'googleGcs' (awsS3, azureStorage, etc. to be available as well).
# techdocs.publisher.type can be - 'local' or 'googleGcs' or 'awsS3' (azureStorage to be available in future).
# When set to 'local', techdocs-backend will create a 'static' directory at its root to store generated documentation files.
# When set to 'googleGcs', techdocs-backend will use a Google Cloud Storage Bucket to store generated documentation files.
# When set to 'awsS3', techdocs-backend will use an Amazon Web Service (AWS) S3 bucket to store generated documentation files.
type: 'local'
# Required when techdocs.publisher.type is set to 'googleGcs'. Skip otherwise.
googleGcs:
# An API key is required to write to a storage bucket.
credentials:
$file: '/path/to/google_application_credentials.json',
$file: '/path/to/google_application_credentials.json'
# Your GCP Project ID where the Cloud Storage Bucket is hosted.
projectId: 'gcp-project-id'
# Cloud Storage Bucket Name
bucketName: 'techdocs-storage',
bucketName: 'techdocs-storage'
# Required when techdocs.publisher.type is set to 'awsS3'. Skip otherwise.
awsS3:
# An API key is required to write to a storage bucket.
credentials:
accessKeyId:
$env: TECHDOCS_AWSS3_ACCESS_KEY_ID_CREDENTIAL
secretAccessKey:
$env: TECHDOCS_AWSS3_SECRET_ACCESS_KEY_CREDENTIAL
region:
$env: AWSS3_REGION
# AWS S3 Bucket Name
bucketName: 'techdocs-storage'
```
@@ -41,7 +41,7 @@ setup for free.
### Manually add documentation setup to already existing repository
Prerequisities:
Prerequisites:
- An existing component
[registered in backstage](../software-catalog/index.md#adding-components-to-the-catalog)
+13 -22
View File
@@ -72,12 +72,8 @@ add the following
```typescript
import {
createRouter,
DirectoryPreparer,
Preparers,
Generators,
TechdocsGenerator,
CommonGitPreparer,
UrlPreparer,
Publisher,
} from '@backstage/plugin-techdocs-backend';
import { PluginEnvironment } from '../types';
@@ -90,30 +86,25 @@ export default async function createPlugin({
reader,
}: PluginEnvironment) {
// Preparers are responsible for fetching source files for documentation.
const preparers = new Preparers();
const directoryPreparer = new DirectoryPreparer(logger);
preparers.register('dir', directoryPreparer);
const commonGitPreparer = new CommonGitPreparer(logger);
preparers.register('github', commonGitPreparer);
preparers.register('gitlab', commonGitPreparer);
preparers.register('azure/api', commonGitPreparer);
const urlPreparer = new UrlPreparer(reader, logger);
preparers.register('url', urlPreparer);
const preparers = await Preparers.fromConfig(config, {
logger,
reader,
});
// Generators are used for generating documentation sites.
const generators = new Generators();
const techdocsGenerator = new TechdocsGenerator(logger, config);
generators.register('techdocs', techdocsGenerator);
const generators = await Generators.fromConfig(config, {
logger,
});
// Publishers are used for
// Publisher is used for
// 1. Publishing generated files to storage
// 2. Fetching files from storage and passing them to TechDocs frontend.
const publisher = Publisher.fromConfig(config, logger, discovery);
const publisher = await Publisher.fromConfig(config, {
logger,
discovery,
});
// Docker client used by the generators.
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
return await createRouter({
@@ -93,3 +93,175 @@ techdocs:
Your Backstage app is now ready to use Google Cloud Storage for TechDocs, to
store the static generated documentation files.
## Configuring AWS S3 Bucket with TechDocs
Follow the
[official AWS S3 documentation](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html)
for the latest instructions on the following steps involving AWS S3.
**1. Set `techdocs.publisher.type` config in your `app-config.yaml`**
Set `techdocs.publisher.type` to `'awsS3'`.
```yaml
techdocs:
publisher:
type: 'awsS3'
```
**2. AWS Policies**
AWS Policies lets you **control access** to Amazon Web Services (AWS) products
and resources.
Here we will use a user policy **and** a bucket policy to show you the different
possibilities you have but you can use only one.
<img data-zoomable src="../../assets/techdocs/aws-s3.drawio.svg" alt="AWS S3" width="500" />
This is an example of how you can manage your policies:
a. Admin user creates a **bucket policy** granting a set of permissions to our
TechDocs user.
b. Admin user attaches a **user policy** to the TechDocs user granting
additional permissions.
c. TechDocs User then tries permissions granted via both the **bucket** policy
and the **user** policy.
**2.1 Creation**
**2.1.1 Create an Admin user** (if you don't have one yet)
Create an **administrator user** account `ADMIN_USER` and grant it administrator
privileges by attaching a user policy giving the account **full access**.
Note down the Admin User credentials and IAM User Sign-In URL as you will need
to use this information in the next step.
**2.1.2 Create an AWS S3 Bucket**
Using the credentials of your Admin User `ADMIN_USER`, and the special IAM user
sign-in URL, create a dedicated **bucket** for TechDocs sites. techdocs-backend
will publish documentation to this bucket. TechDocs will fetch files from here
to serve documentation in Backstage.
Set the name of the bucket to `techdocs.publisher.awsS3.bucketName`.
```yaml
techdocs:
publisher:
type: 'awsS3'
awsS3:
bucketName: 'name-of-techdocs-storage-bucket'
```
**2.1.3 Create the `TechDocs` user**
This user will be used to interact with your bucket, it will only have
permissions to **get - put** objects.
In the IAM console, do the following:
- Create a new user, `TechDocs`
- Note down the TechDocs User credentials
- Note down the Amazon Resource Name (ARN) for the TechDocs user. In the IAM
console, select the TechDocs user, and you can find the user ARN in the
Summary tab.
**2.2 Attach policies**
Remember that you can use Bucket policy **or** User policy.
Just make sure that you grant all the permissions to the TechDocs user:
`3:PutObject`, `s3:GetObject`, `s3:ListBucket` and `s3:GetBucketLocation`.
**2.2.1 Create the bucket policy**
You now have to attach the following policy to your bucket in the Permission
section:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "statement1",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::YOUR_ACCOUNT_ID:user/TechDocs"
},
"Action": ["s3:GetBucketLocation", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::name-of-techdocs-storage-bucket"]
},
{
"Sid": "statement2",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::YOUR_ACCOUNT_ID:user/TechDocs"
},
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::name-of-techdocs-storage-bucket/*"]
}
]
}
```
- The first statement grants **TechDocs User** the bucket operation permissions
`s3:GetBucketLocation` and `s3:ListBucket` which are permissions required by
the console.
- The second statement grants the `s3:GetObject` permission.
(**NOTE :** if you do not use the user policy defined below you must also add
the `s3:PutObject` permission to allow the TechDocs user to add objects.)
**2.2.2 Create the user policy**
Create an inline policy for the TechDocs user by using the following policy:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PermissionForObjectOperations",
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": ["arn:aws:s3:::name-of-techdocs-storage-bucket/*"]
}
]
}
```
See more details in the section
[Working with Inline Policies](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_manage.html).
Now you need to fill in the environment variables with the `TechDocs` User
credentials. You can also specify a region if you want to accesses the resources
in a specific region. Otherwise no region will be selected by default.
```properties
TECHDOCS_AWSS3_ACCESS_KEY_ID_CREDENTIAL="TECHDOCS_ACCESS_KEY_ID"
TECHDOCS_AWSS3_SECRET_ACCESS_KEY_CREDENTIAL="TECHDOCS_SECRET_ACCESS_KEY"
AWSS3_REGION="" // Optional
```
Make it available in your Backstage server and/or your local development server
and set it in the app config techdocs.publisher.awsS3.
```yaml
techdocs:
publisher:
type: 'awsS3'
awsS3:
credentials:
accessKeyId:
$env: TECHDOCS_AWSS3_ACCESS_KEY_ID_CREDENTIAL
secretAccessKey:
$env: TECHDOCS_AWSS3_SECRET_ACCESS_KEY_CREDENTIAL
region:
$env: AWSS3_REGION
```
**3. That's it!**
Your Backstage app is now ready to use AWS S3 for TechDocs, to store the static
generated documentation files.
+1 -1
View File
@@ -9,7 +9,7 @@ Powered by a centralized service catalog, Backstage restores order to your micro
[Backstage](https://backstage.io/) is an open platform for building developer
portals. Powered by a centralized service catalog, Backstage restores order to
your microservices and infrastructure. So your product teams can ship
your microservices and infrastructure and enables your product teams to ship
high-quality code quickly — without compromising autonomy.
Backstage unifies all your infrastructure tooling, services, and documentation
+1 -1
View File
@@ -33,4 +33,4 @@ that someone else will pick up the work.
If your plugin isn't supposed to live as a standalone page, but rather needs to
be presented as a part of a Service Catalog (e.g. a separate tab or a card on an
"Overview" tab), then check out
[the instruction](integrating-plugin-into-service-catalog.md). on how to do it.
[the instruction](integrating-plugin-into-service-catalog.md) on how to do it.
+42 -33
View File
@@ -16,15 +16,15 @@ frameworks and libraries like [Mocha](https://mochajs.org/),
Running all tests:
yarn test-react
yarn test
Running an individual test (e.g. `MyComponent.test.js`):
yarn test-react MyComponent
yarn test MyComponent
To run both `MyComponent.test.js` and `MyControl.test.js` suite of tests:
yarn test-react MyCo
yarn test MyCo
Note: if `console.logs` are not appearing, run only the individual test you are
working on.
@@ -52,12 +52,12 @@ render React components.
TODO.
# Writing Unit Tests
## Writing Unit Tests
The following principles are good guides for determining if you are writing high
quality frontend unit tests.
## Bad Unit Test Principle
### Bad Unit Test Principle
> No unit test is better than a bad one.
@@ -69,7 +69,7 @@ Writing a poor unit test:
- Adds to future work by requiring updates to the unit test for irrelevant code
changes.
## Input/Output Principle
### Input/Output Principle
> A unit test verifies an output matches an expected input.
@@ -77,7 +77,7 @@ For backend, this would be that when you provide configuration X, then the
object responds with Y. For frontend, this would be that when you provide
properties X to a component, then the visual functionality responds with Y.
## Blackbox Principle
### Blackbox Principle
> A good unit test does not tell the object how it should do its job but should
> only compare inputs to outputs.
@@ -86,7 +86,7 @@ Consider a unit test for a form. A good unit test would not test the order of
the form fields. Instead, it would verify that the inputs to the form fields
lead to a certain backend call when submit is clicked.
## Scalability Principle
### Scalability Principle
> Unit test quality is directly proportionate to how much code can change
> without having to touch the unit test.
@@ -97,7 +97,7 @@ to the code, you have to update the unit test. A good unit test suite allows a
lot of flexibility in _how_ the code is written so that future refactoring can
occur without having to touch the original unit tests.
## Increasing Complexity Principle
### Increasing Complexity Principle
> The ordering of unit tests in a suite should proceed from least specific to
> most specific.
@@ -116,7 +116,7 @@ throwing an error saying that output was incorrect will lead the next developer
into thinking they may have broken the entire functionality of the object rather
than simply letting them know they had an invalid input.
## Broken Functionality Principle
### Broken Functionality Principle
> Generally, a unit test should not test exactly how the output appears, it
> should test that the functionality has an expected _general_ response to an
@@ -131,7 +131,7 @@ test a slightly different color on the button the unit test will break. A better
unit test would verify that the button's CSS classname is assigned properly on
hover or test for something completely different.
## Example: Loading Indicator
### Example: Loading Indicator
A classic unit test on frontends is verifying a loading indicator displays when
a backend request is being made.
@@ -192,11 +192,14 @@ returns a result or displays an error or console message, like so:
**`StringUtil ellipsis`**
export function ellipsis(text, maxLength, midCharIx = 0, ellipsis = '...') {
// Do something blackbox. We should not care about the internals, only inputs and outputs.
...
return someFinalValue;
}
```js
export function ellipsis(text, maxLength, midCharIx = 0, ellipsis = '...') {
// Do something blackbox. We should not care about the internals,
// only inputs and outputs.
...
return someFinalValue;
}
```
There are four things to test for in a utility function:
@@ -207,30 +210,36 @@ There are four things to test for in a utility function:
> Handle Invalid Input (handle thrown errors):
it('Throws an error on improper arguments', () => {
expect(() => {
ellipsis();
}).toThrowError('Expected \'text\' to be defined');
});
```js
it('Throws an error on improper arguments', () => {
expect(() => {
ellipsis();
}).toThrowError("Expected 'text' to be defined");
});
```
> Verify default input arguments:
it('Works with defaults', () => {
expect(ellipsis('Hello world', 3)).toBe('Hel...');
expect(ellipsis('', 3)).toBe('');
expect(ellipsis('H', 3)).toBe('H');
expect(ellipsis('Hello', 5)).toBe('Hello');
});
```js
it('Works with defaults', () => {
expect(ellipsis('Hello world', 3)).toBe('Hel...');
expect(ellipsis('', 3)).toBe('');
expect(ellipsis('H', 3)).toBe('H');
expect(ellipsis('Hello', 5)).toBe('Hello');
});
```
> Verify output for expected input arguments:
This is especially true for edge cases!
it('Works with midCharIx', () => {
expect(ellipsis('Hello world', 3, 6)).toBe('...o w...');
expect(ellipsis('', 3, 6)).toBe('');
expect(ellipsis('Backstage is amazing', 4, 10)).toBe('...e is...');
});
```js
it('Works with midCharIx', () => {
expect(ellipsis('Hello world', 3, 6)).toBe('...o w...');
expect(ellipsis('', 3, 6)).toBe('');
expect(ellipsis('Backstage is amazing', 4, 10)).toBe('...e is...');
});
```
## Non-React Classes
@@ -372,4 +381,4 @@ IDE.
In most cases, we have found that using `console.log` works well.
Note: if your console.logs are not being displayed, focus your specific unit
test from the command line by running them like so `yarn test-react MyTest`.
test from the command line by running them like so `yarn test MyTest`.
+2 -2
View File
@@ -41,8 +41,8 @@ the code.
appreciate contributions in here and encourage them being kept up to date.
- [`docs/`](https://github.com/backstage/backstage/tree/master/docs) - This is
where we keep all of our documentation Markdown files. These ends up on
http://backstage.io/docs. Just keep in mind that changes to the
where we keep all of our documentation Markdown files. These end up on
https://backstage.io/docs. Just keep in mind that changes to the
[`sidebars.json`](https://github.com/backstage/backstage/blob/master/microsite/sidebars.json)
file may be needed as sections are added/removed.
+14
View File
@@ -0,0 +1,14 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: kafka-test
description: Kafka Test
annotations:
kafka.apache.org/consumer-group: consumer
tags:
- java
- data
spec:
type: service
lifecycle: experimental
owner: architecture
@@ -0,0 +1,42 @@
---
title: Announcing the Backstage Stability Index
author: Patrik Oldsberg, Spotify
authorURL: https://github.com/Rugvip
---
**TL;DR** Backstage is heading out of alpha and moving onto the path to stable releases and an eventual version 1.0. As the community and ecosystem continue to grow at an increasing rate, we want to provide a solid foundation for everyone building things in, with, and around Backstage. So, today were introducing the [Stability Index](https://backstage.io/docs/overview/stability-index) — a simple way to find out how likely (or unlikely) a specific package or plugin inside Backstage might be updated with major changes. By indicating the reliability of key features and APIs, this quick reference will help contributors and adopters better plan and coordinate their development efforts going forward.
![Animation cycling between stability index scores](assets/2020-12-22/stability-index-hero.gif)
<!--truncate-->
Backstage has a great and growing community of users and contributors. With so much valuable feedback, this inevitably means that sometimes we will go back and revise design decisions that introduce breaking changes. Furthermore, as we continue to ship new features and systems, well continue this cycle of iterating, gathering feedback, and iterating again before reaching a stable API.
This rapid evolution can create uncertainty around which parts of the project are considered stable and which are not. Regardless of whether you're contributing to the project directly, setting up your own Backstage, or building plugins, you will be using various parts of the project that vary in stability. While many API surfaces of Backstage are reasonably solid and are unlikely to see any big breaking changes, others are more unstable or experimental.
## The Stability Index
In order to tackle the problem of uncertainty, and help align contributors, we have recently introduced a Stability Index. Inspired by a [similar concept with the same name in Node.js](https://nodejs.org/docs/latest-v4.x/api/documentation.html#documentation_stability_index), its a score assigned to subsets of the project, indicating the level of maturity of the API and the commitment to backwards compatibility. However, because of the current phase of the project, we have used a slightly different implementation. Rather than the score indicating a perceived stability, a higher score is instead a commitment to providing a smoother upgrade path for users, both through better documentation and backwards compatibility. Importantly, the Stability Index does not supersede [semantic versioning](https://semver.org/) (or semver), meaning we will still adhere to semver and only do breaking changes in minor releases as long as we are on 0.x.
You can find more details about the scores on the [Stability Index](https://backstage.io/docs/overview/stability-index) page, but the following is a TL;DR of the 03 scores:
- **0** — There's a changelog entry.
- **1** — There's a migration guide.
- **2** — Two weeks of deprecation.
- **3** — Three months of deprecation.
The Stability Index has been tailored for the phase that the project is currently in, and we do not intend to keep it in place in its current form forever. It is, for example, likely that we in the future will increase the deprecation time limits.
## Staying up to date
In addition to the Stability Index, we have also recently adopted a weekly release cadence where we cut new releases every Thursday. Along with the two-week deprecation period of many of the API surfaces in Backstage, there is an excellent opportunity to form a routine to update your own Backstage projects either weekly or biweekly.
In order to make the update process easier, weve added a new command to the Backstage CLI that allows you to easily update your project to use the latest Backstage release. The command is called `version:bump` and is executed in the root of your project using the CLI. You can see what it looks like in action below.
![Output of the `backstage-cli` `version:bump` command](assets/2020-12-22/versions-bump.png)
## Impact
The Stability Index has already been active for a couple of weeks, and we have immediately seen an impact on the consideration and care taken when working in parts of the project that we have committed to keeping more stable. Both members of the core maintainer team and contributors from the community have taken care to roll out updates responsibly, with examples such as [PR #3524](https://github.com/backstage/backstage/pull/3524) and [PR #3465](https://github.com/backstage/backstage/pull/3465).
The core team is working hard to bring more stability to broader parts of Backstage, and engineering it in a way that allows for long-term evolution. There are more learnings to be discovered as we continue evolving Backstage, but we are confident that the Stability Index will allow for a clearer path and a smoother ride for everyone involved!
Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

+1 -1
View File
@@ -2,7 +2,7 @@
title: AWS Lambda
author: roadie.io
authorUrl: https://roadie.io
category: Monitoring
category: Infrastructure
description: View AWS Lambda functions for your components in Backstage.
documentation: https://roadie.io/backstage/plugins/aws-lambda
iconUrl: https://roadie.io/images/logos/lambda.png
@@ -2,7 +2,7 @@
title: Firebase Functions
author: roadie.io
authorUrl: https://roadie.io/
category: Monitoring
category: Infrastructure
description: View Firebase Functions details for your service in Backstage.
documentation: https://roadie.io/backstage/plugins/firebase-functions
iconUrl: https://roadie.io/images/logos/firebase.png
+9
View File
@@ -0,0 +1,9 @@
---
title: FOSSA
author: SDA SE
authorUrl: https://sda.se/
category: Quality
description: View FOSSA license compliance of your components in Backstage.
documentation: https://github.com/backstage/backstage/blob/master/plugins/fossa/README.md
iconUrl: https://avatars0.githubusercontent.com/u/9543448?s=400&v=4
npmPackageName: '@backstage/plugin-fossa'
+1 -1
View File
@@ -1,5 +1,5 @@
---
title: PageDuty
title: PagerDuty
author: Spotify
authorUrl: https://github.com/spotify
category: Monitoring
+1 -1
View File
@@ -16,7 +16,7 @@
"devDependencies": {
"@spotify/prettier-config": "^9.0.0",
"docusaurus": "^2.0.0-alpha.378053ac5",
"js-yaml": "^3.14.1",
"js-yaml": "^4.0.0",
"prettier": "^2.2.1"
},
"prettier": "@spotify/prettier-config"
+10 -14
View File
@@ -17,22 +17,18 @@ const Background = props => {
<Block small className="stripe-bottom bg-black-grey">
<Block.Container style={{ justifyContent: 'flex-start' }}>
<Block.TextBox>
<Block.Title>See us in action.</Block.Title>
<Block.Title>See us in action</Block.Title>
<Block.Paragraph>
To illustrate the potential of Backstage, were showing you{' '}
<a href="https://labs.spotify.com/2020/04/21/how-we-use-backstage-at-spotify/">
how we use it here at Spotify
To explore the UI and basic features of Backstage firsthand, go
to: <a href="https://demo.backstage.io">demo.backstage.io</a>.
</Block.Paragraph>
<Block.Paragraph>
Watch the videos below to get an introduction to Backstage and to
see how we use different plugins to customize{' '}
<a href="https://engineering.atspotify.com/2020/04/21/how-we-use-backstage-at-spotify/">
our internal version of Backstage at Spotify
</a>
. The videos below feature the custom plugins and templates that
we built for our internal version of Backstage. You can use
Backstage to build the developer portal you want integrating the
tools that you use in your own infrastructure ecosystem. (Or get
started by building an open source plugin for Backstage that
everyone can use, like our{' '}
<a href="https://backstage.io/blog/2020/04/06/lighthouse-plugin">
Lighthouse Plugin
</a>
.)
.
</Block.Paragraph>
</Block.TextBox>
<Block.Graphics>
+2 -2
View File
@@ -27,8 +27,8 @@ class Index extends React.Component {
</Block.Title>
<Block.Paragraph>
Powered by a centralized service catalog, Backstage restores
order to your infrastructure. So your product teams can ship
high-quality code quickly without compromising autonomy.
order to your infrastructure and enables your product teams to
ship high-quality code quickly without compromising autonomy.
</Block.Paragraph>
<Block.LinkButton
href={'https://github.com/backstage/backstage#getting-started'}
+1 -1
View File
@@ -16,7 +16,7 @@ const {
const pluginsDirectory = require('path').join(process.cwd(), 'data/plugins');
const pluginMetadata = fs
.readdirSync(pluginsDirectory)
.map(file => yaml.safeLoad(fs.readFileSync(`./data/plugins/${file}`, 'utf8')))
.map(file => yaml.load(fs.readFileSync(`./data/plugins/${file}`, 'utf8')))
.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase()));
const truncate = text =>
text.length > 170 ? text.substr(0, 170) + '...' : text;
+13 -1
View File
@@ -1124,6 +1124,11 @@ argparse@^1.0.10, argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
arr-diff@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
@@ -3921,7 +3926,7 @@ js-tokens@^3.0.2:
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
js-yaml@^3.13.1, js-yaml@^3.14.1, js-yaml@^3.8.1:
js-yaml@^3.13.1, js-yaml@^3.8.1:
version "3.14.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
@@ -3929,6 +3934,13 @@ js-yaml@^3.13.1, js-yaml@^3.14.1, js-yaml@^3.8.1:
argparse "^1.0.7"
esprima "^4.0.0"
js-yaml@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f"
integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==
dependencies:
argparse "^2.0.1"
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
+1 -1
View File
@@ -24,7 +24,7 @@
"docker-build": "yarn tsc && yarn workspace example-backend build-image",
"create-plugin": "backstage-cli create-plugin --scope backstage --no-private",
"remove-plugin": "backstage-cli remove-plugin",
"release": "changeset version && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' && yarn install --frozen-lockfile",
"release": "changeset version && yarn diff --yes && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' && yarn install --frozen-lockfile",
"prettier:check": "prettier --check .",
"lerna": "lerna",
"storybook": "yarn workspace storybook start",
+45
View File
@@ -1,5 +1,50 @@
# example-app
## 0.2.9
### Patch Changes
- Updated dependencies [ab0892358]
- Updated dependencies [37a7d26c4]
- Updated dependencies [8e083f41f]
- Updated dependencies [88da267cc]
- Updated dependencies [9c09a364f]
- Updated dependencies [01707438b]
- Updated dependencies [edb7d0775]
- Updated dependencies [818d45e94]
- Updated dependencies [0588be01f]
- Updated dependencies [b8abdda57]
- Updated dependencies [b7a124883]
- Updated dependencies [bc909178d]
- Updated dependencies [947d3c269]
- @backstage/plugin-cost-insights@0.5.5
- @backstage/plugin-tech-radar@0.3.2
- @backstage/cli@0.4.5
- @backstage/plugin-scaffolder@0.3.6
- @backstage/plugin-sentry@0.3.2
- @backstage/plugin-catalog@0.2.10
- @backstage/plugin-search@0.2.5
- @backstage/plugin-catalog-import@0.3.3
- @backstage/plugin-pagerduty@0.2.5
## 0.2.8
### Patch Changes
- Updated dependencies [19554f6d6]
- Updated dependencies [1dc445e89]
- Updated dependencies [342270e4d]
- Updated dependencies [7d72f9b09]
- @backstage/cli@0.4.3
- @backstage/plugin-github-actions@0.2.6
- @backstage/plugin-scaffolder@0.3.5
- @backstage/core@0.4.2
- @backstage/test-utils@0.1.6
- @backstage/plugin-graphiql@0.2.3
- @backstage/plugin-catalog@0.2.8
- @backstage/plugin-pagerduty@0.2.4
- @backstage/plugin-techdocs@0.5.1
## 0.2.7
### Patch Changes
+16 -16
View File
@@ -1,40 +1,39 @@
{
"name": "example-app",
"version": "0.2.7",
"version": "0.2.9",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.6.0",
"@backstage/cli": "^0.4.2",
"@backstage/core": "^0.4.1",
"@backstage/cli": "^0.4.5",
"@backstage/core": "^0.4.2",
"@backstage/plugin-api-docs": "^0.4.1",
"@backstage/plugin-catalog": "^0.2.7",
"@backstage/plugin-catalog-import": "^0.3.2",
"@backstage/plugin-catalog": "^0.2.10",
"@backstage/plugin-catalog-import": "^0.3.3",
"@backstage/plugin-circleci": "^0.2.5",
"@backstage/plugin-cloudbuild": "^0.2.5",
"@backstage/plugin-cost-insights": "^0.5.2",
"@backstage/plugin-cost-insights": "^0.5.5",
"@backstage/plugin-explore": "^0.2.2",
"@backstage/plugin-gcp-projects": "^0.2.2",
"@backstage/plugin-github-actions": "^0.2.5",
"@backstage/plugin-github-actions": "^0.2.6",
"@backstage/plugin-gitops-profiles": "^0.2.2",
"@backstage/plugin-graphiql": "^0.2.2",
"@backstage/plugin-graphiql": "^0.2.3",
"@backstage/plugin-org": "^0.3.2",
"@backstage/plugin-jenkins": "^0.3.4",
"@backstage/plugin-kafka": "^0.1.0",
"@backstage/plugin-kubernetes": "^0.3.3",
"@backstage/plugin-lighthouse": "^0.2.6",
"@backstage/plugin-newrelic": "^0.2.2",
"@backstage/plugin-org": "^0.3.2",
"@backstage/plugin-pagerduty": "0.2.3",
"@backstage/plugin-pagerduty": "0.2.5",
"@backstage/plugin-register-component": "^0.2.5",
"@backstage/plugin-rollbar": "^0.2.7",
"@backstage/plugin-scaffolder": "^0.3.4",
"@backstage/plugin-search": "^0.2.4",
"@backstage/plugin-sentry": "^0.3.1",
"@backstage/plugin-tech-radar": "^0.3.1",
"@backstage/plugin-techdocs": "^0.5.0",
"@backstage/plugin-scaffolder": "^0.3.6",
"@backstage/plugin-sentry": "^0.3.2",
"@backstage/plugin-search": "^0.2.5",
"@backstage/plugin-tech-radar": "^0.3.2",
"@backstage/plugin-techdocs": "^0.5.1",
"@backstage/plugin-user-settings": "^0.2.3",
"@backstage/plugin-welcome": "^0.2.3",
"@backstage/test-utils": "^0.1.5",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -54,6 +53,7 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/test-utils": "^0.1.6",
"@testing-library/cypress": "^7.0.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+2 -2
View File
@@ -29,7 +29,7 @@ import { hot } from 'react-hot-loader/root';
import { providers } from './identityProviders';
import { Router as CatalogRouter } from '@backstage/plugin-catalog';
import { Router as DocsRouter } from '@backstage/plugin-techdocs';
import { GraphiQLPage } from '@backstage/plugin-graphiql';
import { Router as GraphiQLRouter } from '@backstage/plugin-graphiql';
import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar';
import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse';
import { Router as RegisterComponentRouter } from '@backstage/plugin-register-component';
@@ -81,7 +81,7 @@ const AppRoutes = () => (
path="/tech-radar"
element={<TechRadarRouter width={1500} height={800} />}
/>
<Route path="/graphiql" element={<GraphiQLPage />} />
<Route path="/graphiql" element={<GraphiQLRouter />} />
<Route path="/lighthouse/*" element={<LighthouseRouter />} />
<Route
path="/register-component"
-1
View File
@@ -14,7 +14,6 @@
* limitations under the License.
*/
// eslint-disable-next-line monorepo/no-internal-import
import '@backstage/cli/asset-types';
import React from 'react';
import ReactDOM from 'react-dom';
+13
View File
@@ -1,5 +1,18 @@
# @backstage/backend-common
## 0.4.2
### Patch Changes
- 5ecd50f8a: Fix HTTPS certificate generation and add new config switch, enabling it simply by setting `backend.https = true`. Also introduces caching of generated certificates in order to avoid having to add a browser override every time the backend is restarted.
- 00042e73c: Moving the Git actions to isomorphic-git instead of the node binding version of nodegit
- 0829ff126: Tweaked development log formatter to include extra fields at the end of each log line
- 036a84373: Provide support for on-prem azure devops
- Updated dependencies [ad5c56fd9]
- Updated dependencies [036a84373]
- @backstage/config-loader@0.4.1
- @backstage/integration@0.1.5
## 0.4.1
### Patch Changes
+35 -20
View File
@@ -32,26 +32,41 @@ export interface Config {
port?: string | number;
};
/** HTTPS configuration for the backend. If omitted the backend will serve HTTP */
https?: {
/** Certificate configuration or parameters for generating a self-signed certificate */
certificate?:
| {
/** Algorithm to use to generate a self-signed certificate */
algorithm: string;
keySize?: number;
days?: number;
}
| {
/** PEM encoded certificate. Use $file to load in a file */
cert: string;
/**
* PEM encoded certificate key. Use $file to load in a file.
* @visibility secret
*/
key: string;
};
};
/**
* HTTPS configuration for the backend. If omitted the backend will serve HTTP.
*
* Setting this to `true` will cause self-signed certificates to be generated, which
* can be useful for local development or other non-production scenarios.
*/
https?:
| true
| {
/**
* Certificate configuration or parameters for generating a self-signed certificate
*
* Setting parameters for self-signed certificates is deprecated and will be removed in
* the future, set `backend.https = true` instead.
*/
certificate?:
| {
/** Algorithm to use to generate a self-signed certificate */
algorithm?: string;
keySize?: number;
days?: number;
attributes: {
commonName: string;
};
}
| {
/** PEM encoded certificate. Use $file to load in a file */
cert: string;
/**
* PEM encoded certificate key. Use $file to load in a file.
* @visibility secret
*/
key: string;
};
};
/** Database connection configuration, select database type using the `client` field */
database:
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.4.1",
"version": "0.4.2",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -31,8 +31,8 @@
"dependencies": {
"@backstage/cli-common": "^0.1.1",
"@backstage/config": "^0.1.2",
"@backstage/config-loader": "^0.4.0",
"@backstage/integration": "^0.1.4",
"@backstage/config-loader": "^0.4.1",
"@backstage/integration": "^0.1.5",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
"archiver": "^5.0.2",
@@ -45,6 +45,7 @@
"fs-extra": "^9.0.1",
"git-url-parse": "^11.4.3",
"helmet": "^4.0.0",
"isomorphic-git": "^1.8.0",
"knex": "^0.21.6",
"lodash": "^4.17.15",
"logform": "^2.1.1",
@@ -65,9 +66,9 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.4.2",
"@backstage/cli": "^0.4.5",
"@backstage/test-utils": "^0.1.5",
"@types/archiver": "^3.1.1",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
"@types/concat-stream": "^1.6.0",
"@types/fs-extra": "^9.0.3",
@@ -81,7 +82,6 @@
"@types/tar": "^4.0.3",
"@types/unzipper": "^0.10.3",
"@types/webpack-env": "^1.15.2",
"@types/yaml": "^1.9.7",
"get-port": "^5.1.1",
"http-errors": "^1.7.3",
"jest": "^26.0.1",
+1
View File
@@ -24,3 +24,4 @@ export * from './reading';
export * from './service';
export * from './paths';
export * from './hot';
export * from './scm';
@@ -17,19 +17,23 @@ import * as winston from 'winston';
import { TransformableInfo } from 'logform';
const coloredTemplate = (info: TransformableInfo) => {
const { timestamp, level, message, plugin, service } = info;
const { timestamp, level, message, plugin, service, ...fields } = info;
const colorizer = winston.format.colorize();
const prefix = plugin || service;
const timestampColor = colorizer.colorize('timestamp', timestamp);
const prefixColor = colorizer.colorize('prefix', prefix);
return `${timestampColor} ${prefixColor} ${level} ${message}`;
const extraFields = Object.entries(fields)
.map(([key, value]) => `${colorizer.colorize('field', `${key}`)}=${value}`)
.join(' ');
return `${timestampColor} ${prefixColor} ${level} ${message} ${extraFields}`;
};
export const coloredFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.colorize({
colors: { timestamp: 'dim', prefix: 'blue' },
colors: { timestamp: 'dim', prefix: 'blue', field: 'cyan' },
}),
winston.format.printf(coloredTemplate),
);
@@ -47,13 +47,7 @@ export class AzureUrlReader implements UrlReader {
constructor(
private readonly options: AzureIntegrationConfig,
private readonly deps: { treeResponseFactory: ReadTreeResponseFactory },
) {
if (options.host !== 'dev.azure.com') {
throw Error(
`Azure integration currently only supports 'dev.azure.com', tried to use host '${options.host}'`,
);
}
}
) {}
async read(url: string): Promise<Buffer> {
const builtUrl = getAzureFileFetchUrl(url);
+321
View File
@@ -0,0 +1,321 @@
/*
* Copyright 2020 Spotify AB
*
* 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.
*/
jest.mock('isomorphic-git');
jest.mock('isomorphic-git/http/node');
jest.mock('fs-extra');
import * as isomorphic from 'isomorphic-git';
import { Git } from './git';
import http from 'isomorphic-git/http/node';
import fs from 'fs-extra';
describe('Git', () => {
beforeEach(() => {
jest.resetAllMocks();
});
describe('add', () => {
it('should call isomorphic-git add with the correct arguments', async () => {
const git = Git.fromAuth({});
const dir = 'mockdirectory';
const filepath = 'mockfile/path';
await git.add({ dir, filepath });
expect(isomorphic.add).toHaveBeenCalledWith({
fs,
dir,
filepath,
});
});
});
describe('addRemote', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const git = Git.fromAuth({});
const dir = 'mockdirectory';
const remote = 'origin';
const url = 'git@github.com/something/sads';
await git.addRemote({ dir, remote, url });
expect(isomorphic.addRemote).toHaveBeenCalledWith({
fs,
dir,
remote,
url,
});
});
});
describe('commit', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const git = Git.fromAuth({});
const dir = 'mockdirectory';
const message = 'Inital Commit';
const author = {
name: 'author',
email: 'test@backstage.io',
};
const committer = {
name: 'comitter',
email: 'test@backstage.io',
};
await git.commit({ dir, message, author, committer });
expect(isomorphic.commit).toHaveBeenCalledWith({
fs,
dir,
message,
author,
committer,
});
});
});
describe('clone', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const url = 'http://github.com/some/repo';
const dir = '/some/mock/dir';
const auth = {
username: 'blob',
password: 'hunter2',
};
const git = Git.fromAuth(auth);
await git.clone({ url, dir });
expect(isomorphic.clone).toHaveBeenCalledWith({
fs,
http,
url,
dir,
singleBranch: true,
depth: 1,
onProgress: expect.any(Function),
headers: {
'user-agent': 'git/@isomorphic-git',
},
onAuth: expect.any(Function),
});
});
it('should pass a function that returns the authorization as the onAuth handler', async () => {
const url = 'http://github.com/some/repo';
const dir = '/some/mock/dir';
const auth = {
username: 'blob',
password: 'hunter2',
};
const git = Git.fromAuth(auth);
await git.clone({ url, dir });
const { onAuth } = ((isomorphic.clone as unknown) as jest.Mock<
typeof isomorphic['clone']
>).mock.calls[0][0]!;
expect(onAuth()).toEqual(auth);
});
});
describe('currentBranch', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const dir = '/some/mock/dir';
const fullName = true;
const git = Git.fromAuth({});
await git.currentBranch({ dir, fullName });
expect(isomorphic.currentBranch).toHaveBeenCalledWith({
fs,
dir,
fullname: true,
});
await git.currentBranch({ dir });
expect(isomorphic.currentBranch).toHaveBeenCalledWith({
fs,
dir,
fullname: false,
});
});
});
describe('fetch', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const remote = 'http://github.com/some/repo';
const dir = '/some/mock/dir';
const auth = {
username: 'blob',
password: 'hunter2',
};
const git = Git.fromAuth(auth);
await git.fetch({ remote, dir });
expect(isomorphic.fetch).toHaveBeenCalledWith({
fs,
http,
remote,
dir,
onProgress: expect.any(Function),
headers: {
'user-agent': 'git/@isomorphic-git',
},
onAuth: expect.any(Function),
});
});
it('should pass a function that returns the authorization as the onAuth handler', async () => {
const remote = 'http://github.com/some/repo';
const dir = '/some/mock/dir';
const auth = {
username: 'blob',
password: 'hunter2',
};
const git = Git.fromAuth(auth);
await git.fetch({ remote, dir });
const { onAuth } = ((isomorphic.fetch as unknown) as jest.Mock<
typeof isomorphic['fetch']
>).mock.calls[0][0]!;
expect(onAuth()).toEqual(auth);
});
});
describe('init', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const dir = '/some/mock/dir';
const git = Git.fromAuth({});
await git.init({ dir });
expect(isomorphic.init).toHaveBeenCalledWith({
fs,
dir,
});
});
});
describe('merge', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const dir = '/some/mock/dir';
const author = {
name: 'author',
email: 'test@backstage.io',
};
const committer = {
name: 'comitter',
email: 'test@backstage.io',
};
const theirs = 'master';
const ours = 'production';
const git = Git.fromAuth({});
await git.merge({ dir, theirs, ours, author, committer });
expect(isomorphic.merge).toHaveBeenCalledWith({
fs,
dir,
ours,
theirs,
author,
committer,
});
});
});
describe('push', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const remote = 'origin';
const dir = '/some/mock/dir';
const auth = {
username: 'blob',
password: 'hunter2',
};
const git = Git.fromAuth(auth);
await git.push({ dir, remote });
expect(isomorphic.push).toHaveBeenCalledWith({
fs,
http,
remote,
dir,
onProgress: expect.any(Function),
headers: {
'user-agent': 'git/@isomorphic-git',
},
onAuth: expect.any(Function),
});
});
it('should pass a function that returns the authorization as the onAuth handler', async () => {
const remote = 'origin';
const dir = '/some/mock/dir';
const auth = {
username: 'blob',
password: 'hunter2',
};
const git = Git.fromAuth(auth);
await git.push({ remote, dir });
const { onAuth } = ((isomorphic.push as unknown) as jest.Mock<
typeof isomorphic['push']
>).mock.calls[0][0]!;
expect(onAuth()).toEqual(auth);
});
});
describe('readCommit', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const dir = '/some/mock/dir';
const sha = 'as43bd7';
const git = Git.fromAuth({});
await git.readCommit({ dir, sha });
expect(isomorphic.readCommit).toHaveBeenCalledWith({
fs,
dir,
oid: sha,
});
});
});
describe('resolveRef', () => {
it('should call isomorphic-git with the correct arguments', async () => {
const dir = '/some/mock/dir';
const ref = 'as43bd7';
const git = Git.fromAuth({});
await git.resolveRef({ dir, ref });
expect(isomorphic.resolveRef).toHaveBeenCalledWith({
fs,
dir,
ref,
});
});
});
});
+251
View File
@@ -0,0 +1,251 @@
/*
* Copyright 2020 Spotify AB
*
* 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 git, {
ProgressCallback,
MergeResult,
ReadCommitResult,
} from 'isomorphic-git';
import http from 'isomorphic-git/http/node';
import fs from 'fs-extra';
import { Logger } from 'winston';
/*
provider username password
GitHub token 'x-oauth-basic'
GitHub App token 'x-access-token'
BitBucket 'x-token-auth' token
GitLab 'oauth2' token
From : https://isomorphic-git.org/docs/en/onAuth
Azure 'notempty' token
*/
export class Git {
private constructor(
private readonly config: {
username?: string;
password?: string;
logger?: Logger;
},
) {}
async add({
dir,
filepath,
}: {
dir: string;
filepath: string;
}): Promise<void> {
this.config.logger?.info(`Adding file {dir=${dir},filepath=${filepath}}`);
return git.add({ fs, dir, filepath });
}
async addRemote({
dir,
url,
remote,
}: {
dir: string;
remote: string;
url: string;
}): Promise<void> {
this.config.logger?.info(
`Creating new remote {dir=${dir},remote=${remote},url=${url}}`,
);
return git.addRemote({ fs, dir, remote, url });
}
async commit({
dir,
message,
author,
committer,
}: {
dir: string;
message: string;
author: { name: string; email: string };
committer: { name: string; email: string };
}): Promise<string> {
this.config.logger?.info(
`Committing file to repo {dir=${dir},message=${message}}`,
);
return git.commit({ fs, dir, message, author, committer });
}
async clone({ url, dir }: { url: string; dir: string }): Promise<void> {
this.config.logger?.info(`Cloning repo {dir=${dir},url=${url}}`);
return git.clone({
fs,
http,
url,
dir,
singleBranch: true,
depth: 1,
onProgress: this.onProgressHandler(),
headers: {
'user-agent': 'git/@isomorphic-git',
},
onAuth: this.onAuth,
});
}
// https://isomorphic-git.org/docs/en/currentBranch
async currentBranch({
dir,
fullName,
}: {
dir: string;
fullName?: boolean;
}): Promise<string | undefined> {
const fullname = fullName ?? false;
return git.currentBranch({ fs, dir, fullname }) as Promise<
string | undefined
>;
}
// https://isomorphic-git.org/docs/en/fetch
async fetch({
dir,
remote,
}: {
dir: string;
remote?: string;
}): Promise<void> {
const remoteValue = remote ?? 'origin';
this.config.logger?.info(
`Fetching remote=${remoteValue} for repository {dir=${dir}}`,
);
await git.fetch({
fs,
http,
dir,
remote: remoteValue,
onProgress: this.onProgressHandler(),
headers: {
'user-agent': 'git/@isomorphic-git',
},
onAuth: this.onAuth,
});
}
async init({ dir }: { dir: string }): Promise<void> {
this.config.logger?.info(`Init git repository {dir=${dir}}`);
return git.init({
fs,
dir,
});
}
// https://isomorphic-git.org/docs/en/merge
async merge({
dir,
theirs,
ours,
author,
committer,
}: {
dir: string;
theirs: string;
ours?: string;
author: { name: string; email: string };
committer: { name: string; email: string };
}): Promise<MergeResult> {
this.config.logger?.info(
`Merging branch '${theirs}' into '${ours}' for repository {dir=${dir}}`,
);
// If ours is undefined, current branch is used.
return git.merge({
fs,
dir,
ours,
theirs,
author,
committer,
});
}
async push({ dir, remote }: { dir: string; remote: string }) {
this.config.logger?.info(
`Pushing directory to remote {dir=${dir},remote=${remote}}`,
);
return git.push({
fs,
dir,
http,
onProgress: this.onProgressHandler(),
headers: {
'user-agent': 'git/@isomorphic-git',
},
remote: remote,
onAuth: this.onAuth,
});
}
// https://isomorphic-git.org/docs/en/readCommit
async readCommit({
dir,
sha,
}: {
dir: string;
sha: string;
}): Promise<ReadCommitResult> {
return git.readCommit({ fs, dir, oid: sha });
}
// https://isomorphic-git.org/docs/en/resolveRef
async resolveRef({
dir,
ref,
}: {
dir: string;
ref: string;
}): Promise<string> {
return git.resolveRef({ fs, dir, ref });
}
private onAuth = () => ({
username: this.config.username,
password: this.config.password,
});
private onProgressHandler = (): ProgressCallback => {
let currentPhase = '';
return event => {
if (currentPhase !== event.phase) {
currentPhase = event.phase;
this.config.logger?.info(event.phase);
}
const total = event.total
? `${Math.round((event.loaded / event.total) * 100)}%`
: event.loaded;
this.config.logger?.debug(`status={${event.phase},total={${total}}}`);
};
};
static fromAuth = ({
username,
password,
logger,
}: {
username?: string;
password?: string;
logger?: Logger;
}) => new Git({ username, password, logger });
}
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { IconLinkVertical } from './IconLinkVertical';
export { Git } from './git';
@@ -145,7 +145,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
return this;
}
start(): Promise<http.Server> {
async start(): Promise<http.Server> {
const app = express();
const {
port,
@@ -168,16 +168,16 @@ export class ServiceBuilderImpl implements ServiceBuilder {
app.use(notFoundHandler());
app.use(errorHandler());
const server: http.Server = httpsSettings
? await createHttpsServer(app, httpsSettings, logger)
: createHttpServer(app, logger);
return new Promise((resolve, reject) => {
app.on('error', e => {
logger.error(`Failed to start up on port ${port}, ${e}`);
reject(e);
});
const server: http.Server = httpsSettings
? createHttpsServer(app, httpsSettings, logger)
: createHttpServer(app, logger);
const stoppableServer = stoppable(
server.listen(port, host, () => {
logger.info(`Listening on ${host}:${port}`);
@@ -47,14 +47,14 @@ export type CertificateReferenceOptions = {
};
export type CertificateSigningOptions = {
algorithm: string;
algorithm?: string;
size?: number;
days?: number;
attributes?: CertificateAttributes;
attributes: CertificateAttributes;
};
export type CertificateAttributes = {
commonName?: string;
commonName: string;
};
/**
@@ -193,8 +193,26 @@ export function readCspOptions(
* ```
*/
export function readHttpsSettings(config: Config): HttpsSettings | undefined {
const cc = config.getOptionalConfig('https');
const https = config.getOptional('https');
if (https === true) {
const baseUrl = config.getString('baseUrl');
let commonName;
try {
commonName = new URL(baseUrl).hostname;
} catch (error) {
throw new Error(`Invalid backend.baseUrl "${baseUrl}"`);
}
return {
certificate: {
attributes: {
commonName,
},
},
};
}
const cc = config.getOptionalConfig('https');
if (!cc) {
return undefined;
}
@@ -13,11 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs-extra';
import { resolve as resolvePath, dirname } from 'path';
import express from 'express';
import * as http from 'http';
import * as https from 'https';
import { Logger } from 'winston';
import { HttpsSettings } from './config';
import { CertificateSigningOptions, HttpsSettings } from './config';
const ALMOST_MONTH_IN_MS = 25 * 24 * 60 * 60 * 1000;
/**
* Creates a Http server instance based on an Express application.
@@ -45,48 +50,152 @@ export function createHttpServer(
* @returns A Https server instance
*
*/
export function createHttpsServer(
export async function createHttpsServer(
app: express.Express,
httpsSettings: HttpsSettings,
logger?: Logger,
): http.Server {
): Promise<http.Server> {
logger?.info('Initializing https server');
const credentials: { key: string; cert: string } = {
key: '',
cert: '',
};
let credentials: { key: string | Buffer; cert: string | Buffer };
const signingOptions: any = httpsSettings?.certificate;
if (signingOptions?.algorithm !== undefined) {
logger?.info('Generating self-signed certificate with attributes');
const certificateAttributes: Array<any> = Object.entries(
signingOptions.attributes,
).map(([name, value]) => ({ name, value }));
// TODO: Create a type def for selfsigned.
const signatures = require('selfsigned').generate(certificateAttributes, {
algorithm: signingOptions?.algorithm,
keySize: signingOptions?.size || 2048,
days: signingOptions?.days || 30,
});
logger?.info('Bootstrapping self-signed certificate');
credentials.key = signatures.private;
credentials.cert = signatures.cert;
// TODO(Rugvip): remove support for generated certificate params and make this a more straightforward check
if (signingOptions?.attributes) {
credentials = await getGeneratedCertificate(signingOptions, logger);
} else {
logger?.info('Bootstrapping cert from config');
logger?.info('Loading certificate from config');
credentials.key = signingOptions?.key;
credentials.cert = signingOptions?.cert;
credentials = {
key: signingOptions?.key,
cert: signingOptions?.cert,
};
}
if (credentials.key === '' || credentials.cert === '') {
throw new Error('Invalid credentials');
if (!credentials.key || !credentials.cert) {
throw new Error('Invalid HTTPS credentials');
}
return https.createServer(credentials, app) as http.Server;
}
async function getGeneratedCertificate(
options: CertificateSigningOptions,
logger?: Logger,
) {
if (options?.algorithm) {
logger?.warn(
'Certificate generation configuration with parameters in backend.https.certificate is deprecated, set backend.https = true instead',
);
}
const hasModules = await fs.pathExists('node_modules');
let certPath;
if (hasModules) {
certPath = resolvePath(
'node_modules/.cache/backstage-backend/dev-cert.pem',
);
await fs.ensureDir(dirname(certPath));
} else {
certPath = resolvePath('.dev-cert.pem');
}
let cert = undefined;
if (await fs.pathExists(certPath)) {
const stat = await fs.stat(certPath);
const ageMs = Date.now() - stat.ctimeMs;
if (stat.isFile() && ageMs < ALMOST_MONTH_IN_MS) {
cert = await fs.readFile(certPath);
}
}
if (cert) {
logger?.info('Using existing self-signed certificate');
return {
key: cert,
cert: cert,
};
}
logger?.info('Generating new self-signed certificate');
const newCert = await createCertificate(options);
await fs.writeFile(certPath, newCert.cert + newCert.key, 'utf8');
return newCert;
}
async function createCertificate(options: CertificateSigningOptions) {
const attributes: Array<any> = Object.entries(
options.attributes,
).map(([name, value]) => ({ name, value }));
const params = {
algorithm: options?.algorithm || 'sha256',
keySize: options?.size || 2048,
days: options?.days || 30,
extensions: [
{
name: 'keyUsage',
keyCertSign: true,
digitalSignature: true,
nonRepudiation: true,
keyEncipherment: true,
dataEncipherment: true,
},
{
name: 'extKeyUsage',
serverAuth: true,
clientAuth: true,
codeSigning: true,
timeStamping: true,
},
{
name: 'subjectAltName',
altNames: [
{
type: 2, // DNS
value: 'localhost',
},
{
type: 2,
value: 'localhost.localdomain',
},
{
type: 2,
value: '[::1]',
},
{
type: 7, // IP
ip: '127.0.0.1',
},
{
type: 7,
ip: 'fe80::1',
},
...(options.attributes.commonName
? [
{
type: 2, // DNS
value: options.attributes.commonName,
},
]
: []),
],
},
],
};
return new Promise<{ key: string; cert: string }>((resolve, reject) =>
require('selfsigned').generate(
attributes,
params,
(err: Error, bundle: { private: string; cert: string }) => {
if (err) {
reject(err);
} else {
resolve({ key: bundle.private, cert: bundle.cert });
}
},
),
);
}
+25
View File
@@ -1,5 +1,30 @@
# example-backend
## 0.2.10
### Patch Changes
- Updated dependencies [5eb8c9b9e]
- Updated dependencies [7e3451700]
- @backstage/plugin-scaffolder-backend@0.4.0
## 0.2.8
### Patch Changes
- 7cfcd58ee: use node 14 for backend Dockerfile
- Updated dependencies [19554f6d6]
- Updated dependencies [33a82a713]
- Updated dependencies [5de26b9a6]
- Updated dependencies [30d6c78fb]
- Updated dependencies [5084e5039]
- Updated dependencies [a8573e53b]
- Updated dependencies [aed8f7f12]
- @backstage/plugin-scaffolder-backend@0.3.6
- @backstage/plugin-catalog-backend@0.5.1
- @backstage/plugin-techdocs-backend@0.5.0
- example-app@0.2.8
## 0.2.7
### Patch Changes
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:12-buster
FROM node:14-buster
WORKDIR /usr/src/app
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.2.7",
"version": "0.2.10",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -32,19 +32,19 @@
"@backstage/config": "^0.1.2",
"@backstage/plugin-app-backend": "^0.3.3",
"@backstage/plugin-auth-backend": "^0.2.7",
"@backstage/plugin-catalog-backend": "^0.5.0",
"@backstage/plugin-catalog-backend": "^0.5.1",
"@backstage/plugin-graphql-backend": "^0.1.4",
"@backstage/plugin-kubernetes-backend": "^0.2.3",
"@backstage/plugin-kafka-backend": "^0.1.0",
"@backstage/plugin-proxy-backend": "^0.2.3",
"@backstage/plugin-rollbar-backend": "^0.1.5",
"@backstage/plugin-scaffolder-backend": "^0.3.5",
"@backstage/plugin-techdocs-backend": "^0.4.0",
"@backstage/plugin-scaffolder-backend": "^0.4.0",
"@backstage/plugin-techdocs-backend": "^0.5.0",
"@gitbeaker/node": "^25.2.0",
"@octokit/rest": "^18.0.0",
"azure-devops-node-api": "^10.1.1",
"dockerode": "^3.2.1",
"example-app": "^0.2.7",
"example-app": "^0.2.8",
"express": "^4.17.1",
"express-promise-router": "^3.0.3",
"knex": "^0.21.6",
@@ -54,7 +54,7 @@
"winston": "^3.2.1"
},
"devDependencies": {
"@backstage/cli": "^0.4.2",
"@backstage/cli": "^0.4.3",
"@types/dockerode": "^3.2.1",
"@types/express": "^4.17.6",
"@types/express-serve-static-core": "^4.17.5",
+13 -22
View File
@@ -15,12 +15,8 @@
*/
import {
createRouter,
DirectoryPreparer,
Preparers,
Generators,
TechdocsGenerator,
CommonGitPreparer,
UrlPreparer,
Publisher,
} from '@backstage/plugin-techdocs-backend';
import { PluginEnvironment } from '../types';
@@ -33,30 +29,25 @@ export default async function createPlugin({
reader,
}: PluginEnvironment) {
// Preparers are responsible for fetching source files for documentation.
const preparers = new Preparers();
const directoryPreparer = new DirectoryPreparer(logger);
preparers.register('dir', directoryPreparer);
const commonGitPreparer = new CommonGitPreparer(logger);
preparers.register('github', commonGitPreparer);
preparers.register('gitlab', commonGitPreparer);
preparers.register('azure/api', commonGitPreparer);
const urlPreparer = new UrlPreparer(reader, logger);
preparers.register('url', urlPreparer);
const preparers = await Preparers.fromConfig(config, {
logger,
reader,
});
// Generators are used for generating documentation sites.
const generators = new Generators();
const techdocsGenerator = new TechdocsGenerator(logger, config);
generators.register('techdocs', techdocsGenerator);
const generators = await Generators.fromConfig(config, {
logger,
});
// Publishers are used for
// Publisher is used for
// 1. Publishing generated files to storage
// 2. Fetching files from storage and passing them to TechDocs frontend.
const publisher = Publisher.fromConfig(config, logger, discovery);
const publisher = await Publisher.fromConfig(config, {
logger,
discovery,
});
// Docker client used by the generators.
// Docker client (conditionally) used by the generators, based on techdocs.generators config.
const dockerClient = new Docker();
return await createRouter({
+24
View File
@@ -1,5 +1,29 @@
# @backstage/cli
## 0.4.5
### Patch Changes
- 37a7d26c4: Use consistent file extensions for JS output when building packages.
- 818d45e94: Fix detection of external package child directories
- 0588be01f: Add `backend:bundle` command for bundling a backend package with dependencies into a deployment archive.
- b8abdda57: Add color to output from `versions:bump` in order to make it easier to spot changes. Also highlight possible breaking changes and link to changelogs.
- Updated dependencies [ad5c56fd9]
- @backstage/config-loader@0.4.1
## 0.4.4
### Patch Changes
- d45efbc9b: Fix typo in .app.listen.port config schema
## 0.4.3
### Patch Changes
- 19554f6d6: Added Github Actions for Create React App, and allow better imports of files inside a module when they're exposed using `files` in `package.json`
- 7d72f9b09: Fix for `app.listen.host` configuration not properly overriding listening host.
## 0.4.2
### Patch Changes
+8 -9
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.4.2",
"version": "0.4.5",
"private": false,
"publishConfig": {
"access": "public"
@@ -30,7 +30,7 @@
"dependencies": {
"@backstage/cli-common": "^0.1.1",
"@backstage/config": "^0.1.2",
"@backstage/config-loader": "^0.4.0",
"@backstage/config-loader": "^0.4.1",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^3.18.5",
"@lerna/project": "^3.18.0",
@@ -66,7 +66,7 @@
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-jest": "^24.1.0",
"eslint-plugin-jsx-a11y": "^6.2.1",
"eslint-plugin-monorepo": "^0.2.1",
"eslint-plugin-monorepo": "^0.3.2",
"eslint-plugin-react": "^7.12.4",
"eslint-plugin-react-hooks": "^4.0.0",
"fork-ts-checker-webpack-plugin": "^4.0.5",
@@ -111,11 +111,11 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/backend-common": "^0.4.1",
"@backstage/backend-common": "^0.4.2",
"@backstage/config": "^0.1.2",
"@backstage/core": "^0.4.1",
"@backstage/dev-utils": "^0.1.6",
"@backstage/test-utils": "^0.1.5",
"@backstage/core": "^0.4.3",
"@backstage/dev-utils": "^0.1.7",
"@backstage/test-utils": "^0.1.6",
"@backstage/theme": "^0.2.2",
"@types/diff": "^4.0.2",
"@types/fs-extra": "^9.0.1",
@@ -125,7 +125,6 @@
"@types/mini-css-extract-plugin": "^0.9.1",
"@types/mock-fs": "^4.13.0",
"@types/node": "^13.7.2",
"@types/ora": "^3.2.0",
"@types/react-dev-utils": "^9.0.4",
"@types/recursive-readdir": "^2.2.0",
"@types/rollup-plugin-peer-deps-external": "^2.2.0",
@@ -182,7 +181,7 @@
"visibility": "frontend",
"description": "The host that the frontend should be bound to. Only used for local development."
},
"post": {
"port": {
"type": "number",
"visibility": "frontend",
"description": "The port that the frontend should be bound to. Only used for local development."
+1 -1
View File
@@ -38,7 +38,7 @@ export default async (cmd: Command) => {
try {
await createDistWorkspace([pkg.name], {
targetDir: tmpDir,
buildDependencies: Boolean(cmd.build),
buildDependencies: Boolean(cmd.buildDependencies),
buildExcludes: [pkg.name],
parallel: parseParallel(process.env[PARALLEL_ENV_VAR]),
skeleton: SKELETON_FILE,
+7 -3
View File
@@ -45,9 +45,12 @@ export function registerCommands(program: CommanderStatic) {
.action(lazy(() => import('./backend/build').then(m => m.default)));
program
.command('backend:__experimental__bundle__', { hidden: true })
.description('Bundle all backend packages into dist-workspace')
.option('--build', 'Build packages before packing them into the image')
.command('backend:bundle')
.description('Bundle the backend into a deployment archive')
.option(
'--build-dependencies',
'Build all local package dependencies before bundling the backend',
)
.action(lazy(() => import('./backend/bundle').then(m => m.default)));
program
@@ -212,6 +215,7 @@ function lazy(
try {
const actionFunc = await getActionFunc();
await actionFunc(...args);
process.exit(0);
} catch (error) {
exitWithError(error);
@@ -23,6 +23,15 @@ import * as runObj from '../../lib/run';
import bump from './bump';
import { withLogCollector } from '@backstage/test-utils';
// Remove log coloring to simplify log matching
jest.mock('chalk', () => ({
blue: (str: string) => str,
cyan: (str: string) => str,
green: (str: string) => str,
magenta: (str: string) => str,
yellow: (str: string) => str,
}));
const REGISTRY_VERSIONS: { [name: string]: string } = {
'@backstage/core': '1.0.6',
'@backstage/core-api': '1.0.7',
@@ -121,11 +130,15 @@ describe('bump', () => {
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/core-api',
'Some packages are outdated, updating',
'Removing lockfile entry for @backstage/core@^1.0.3 to bump to 1.0.6',
'Removing lockfile entry for @backstage/core-api@^1.0.6 to bump to 1.0.7',
'Removing lockfile entry for @backstage/core-api@^1.0.3 to bump to 1.0.7',
'Bumping @backstage/theme in b to ^2.0.0',
"Running 'yarn install' to install new versions",
'unlocking @backstage/core@^1.0.3 ~> 1.0.6',
'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7',
'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7',
'bumping @backstage/theme in b to ^2.0.0',
'Running yarn install to install new versions',
'⚠️ The following packages may have breaking changes:',
' @backstage/theme',
' https://github.com/backstage/backstage/blob/master/packages/theme/CHANGELOG.md',
'Version bump complete!',
]);
expect(runObj.runPlain).toHaveBeenCalledTimes(3);
+61 -5
View File
@@ -15,6 +15,7 @@
*/
import fs from 'fs-extra';
import chalk from 'chalk';
import semver from 'semver';
import { resolve as resolvePath } from 'path';
import { run } from '../../lib/run';
@@ -35,6 +36,7 @@ const DEP_TYPES = [
type PkgVersionInfo = {
range: string;
target: string;
name: string;
location: string;
};
@@ -69,6 +71,7 @@ export default async () => {
name,
location: pkg.location,
range: `^${target}`, // TODO(Rugvip): Option to use something else than ^?
target,
}),
);
}
@@ -98,9 +101,9 @@ export default async () => {
// Write all discovered version bumps to package.json in this repo
if (versionBumps.size === 0 && unlocked.length === 0) {
console.log('All Backstage packages are up to date!');
console.log(chalk.green('All Backstage packages are up to date!'));
} else {
console.log('Some packages are outdated, updating');
console.log(chalk.yellow('Some packages are outdated, updating'));
console.log();
if (unlocked.length > 0) {
@@ -115,7 +118,9 @@ export default async () => {
if (!removed.has(key)) {
removed.add(key);
console.log(
`Removing lockfile entry for ${name}@${range} to bump to ${target}`,
`${chalk.magenta('unlocking')} ${name}@${chalk.yellow(
range,
)} ~> ${chalk.yellow(target)}`,
);
lockfile.remove(name, range);
}
@@ -123,16 +128,34 @@ export default async () => {
await lockfile.save();
}
const breakingUpdates = new Map<string, { from: string; to: string }>();
await workerThreads(16, versionBumps.entries(), async ([name, deps]) => {
const pkgPath = resolvePath(deps[0].location, 'package.json');
const pkgJson = await fs.readJson(pkgPath);
for (const dep of deps) {
console.log(`Bumping ${dep.name} in ${name} to ${dep.range}`);
console.log(
`${chalk.cyan('bumping')} ${dep.name} in ${chalk.cyan(
name,
)} to ${chalk.yellow(dep.range)}`,
);
for (const depType of DEP_TYPES) {
if (depType in pkgJson && dep.name in pkgJson[depType]) {
const oldRange = pkgJson[depType][dep.name];
pkgJson[depType][dep.name] = dep.range;
// Check if the update was at least a pre-v1 minor or post-v1 major release
const lockfileEntry = lockfile
.get(dep.name)
?.find(entry => entry.range === oldRange);
if (lockfileEntry) {
const from = lockfileEntry.version;
const to = dep.target;
if (!semver.satisfies(to, `^${from}`)) {
breakingUpdates.set(dep.name, { from, to });
}
}
}
}
}
@@ -141,9 +164,42 @@ export default async () => {
});
console.log();
console.log("Running 'yarn install' to install new versions");
console.log(
`Running ${chalk.blue('yarn install')} to install new versions`,
);
console.log();
await run('yarn', ['install']);
if (breakingUpdates.size > 0) {
console.log();
console.log(
chalk.yellow('⚠️ The following packages may have breaking changes:'),
);
console.log();
for (const name of Array.from(breakingUpdates.keys()).sort()) {
console.log(` ${chalk.yellow(name)}`);
let path;
if (name.startsWith('@backstage/plugin-')) {
path = `plugins/${name.replace('@backstage/plugin-', '')}`;
} else if (name.startsWith('@backstage/')) {
path = `packages/${name.replace('@backstage/', '')}`;
}
if (path) {
// TODO(Rugvip): Grab these URLs and paths from package.json, possibly verify existence
// Possibly invent new "changelog" field in package.json or some sh*t.
console.log(
` https://github.com/backstage/backstage/blob/master/${path}/CHANGELOG.md`,
);
}
console.log();
}
} else {
console.log();
}
console.log(chalk.green('Version bump complete!'));
}
console.log();
+2 -2
View File
@@ -62,7 +62,7 @@ export const makeConfigs = async (
output.push({
dir: 'dist',
entryFileNames: 'index.cjs.js',
chunkFileNames: 'cjs/[name]-[hash].js',
chunkFileNames: 'cjs/[name]-[hash].cjs.js',
format: 'commonjs',
sourcemap: true,
});
@@ -71,7 +71,7 @@ export const makeConfigs = async (
output.push({
dir: 'dist',
entryFileNames: 'index.esm.js',
chunkFileNames: 'esm/[name]-[hash].js',
chunkFileNames: 'esm/[name]-[hash].esm.js',
format: 'module',
sourcemap: true,
});
@@ -14,20 +14,27 @@
* limitations under the License.
*/
import * as os from 'os';
import * as path from 'path';
import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin';
describe('LinkedPackageResolvePlugin', () => {
const root = os.platform() === 'win32' ? 'C:\\root' : '/root';
it('should re-write paths for external packages', () => {
const plugin = new LinkedPackageResolvePlugin('/root/repo/node_modules', [
{
name: 'a',
location: '/root/external-a',
},
{
name: '@s/b',
location: '/root/external-b',
},
]);
const plugin = new LinkedPackageResolvePlugin(
path.resolve(root, 'repo/node_modules'),
[
{
name: 'a',
location: path.resolve(root, 'external-a'),
},
{
name: '@s/b',
location: path.resolve(root, 'external-b'),
},
],
);
const tapAsync = jest.fn();
const doResolve = jest.fn();
@@ -50,10 +57,10 @@ describe('LinkedPackageResolvePlugin', () => {
const callbackX = jest.fn();
tap(
{
request: '/root/repo/package/x/src/module.ts',
path: '/root/repo/package/x/src',
request: path.resolve(root, 'repo/package/x/src/module.ts'),
path: path.resolve(root, 'repo/package/x/src'),
context: {
issuer: '/root/repo/package/x/src/index.ts',
issuer: path.resolve(root, 'repo/package/x/src/index.ts'),
},
},
'some-context',
@@ -77,14 +84,31 @@ describe('LinkedPackageResolvePlugin', () => {
expect(callbackFalse).toHaveBeenCalledWith();
expect(doResolve).toHaveBeenCalledTimes(0);
// Internal modules with a path prefix of an external module
const callbackY = jest.fn();
tap(
{
request: path.resolve(root, 'external-aa/src/module.ts'),
path: path.resolve(root, 'external-aa/src'),
context: {
issuer: path.resolve(root, 'external-aa/src/index.ts'),
},
},
'some-context',
callbackY,
);
expect(callbackY).toHaveBeenCalledTimes(1);
expect(callbackY).toHaveBeenCalledWith();
expect(doResolve).toHaveBeenCalledTimes(0);
// External modules have their path and issuer context rewritten, but not the request
const callbackA = jest.fn();
tap(
{
request: '/root/external-a/src/module.ts',
path: '/root/external-a/src',
request: path.resolve(root, 'external-a/src/module.ts'),
path: path.resolve(root, 'external-a/src'),
context: {
issuer: '/root/external-a/src/index.ts',
issuer: path.resolve(root, 'external-a/src/index.ts'),
},
},
'some-context',
@@ -95,13 +119,16 @@ describe('LinkedPackageResolvePlugin', () => {
expect(doResolve).toHaveBeenCalledWith(
resolver.hooks.resolve,
{
request: '/root/external-a/src/module.ts',
path: '/root/repo/node_modules/a/src',
request: path.resolve(root, 'external-a/src/module.ts'),
path: path.resolve(root, 'repo/node_modules/a/src'),
context: {
issuer: '/root/repo/node_modules/a/src/index.ts',
issuer: path.resolve(root, 'repo/node_modules/a/src/index.ts'),
},
},
'resolve /root/external-a/src/module.ts in /root/repo/node_modules/a',
`resolve ${path.resolve(
root,
'external-a/src/module.ts',
)} in ${path.resolve(root, 'repo/node_modules/a')}`,
'some-context',
callbackA,
);
@@ -110,8 +137,8 @@ describe('LinkedPackageResolvePlugin', () => {
const callbackB = jest.fn();
tap(
{
request: '/root/external-b/src/module.ts',
path: '/root/external-b/src',
request: path.resolve(root, 'external-b/src/module.ts'),
path: path.resolve(root, 'external-b/src'),
context: {
issuer: false,
},
@@ -124,13 +151,16 @@ describe('LinkedPackageResolvePlugin', () => {
expect(doResolve).toHaveBeenLastCalledWith(
resolver.hooks.resolve,
{
request: '/root/external-b/src/module.ts',
path: '/root/repo/node_modules/@s/b/src',
request: path.resolve(root, 'external-b/src/module.ts'),
path: path.resolve(root, 'repo/node_modules/@s/b/src'),
context: {
issuer: false,
},
},
'resolve /root/external-b/src/module.ts in /root/repo/node_modules/@s/b',
`resolve ${path.resolve(
root,
'external-b/src/module.ts',
)} in ${path.resolve(root, 'repo/node_modules/@s/b')}`,
'some-context',
callbackB,
);
@@ -16,6 +16,7 @@
import { resolve as resolvePath } from 'path';
import { ResolvePlugin } from 'webpack';
import { isChildPath } from './paths';
import { LernaPackage } from './types';
// Enables proper resolution of packages when linking in external packages.
@@ -40,7 +41,7 @@ export class LinkedPackageResolvePlugin implements ResolvePlugin {
callback: () => void,
) => {
const pkg = this.packages.find(
pkg => data.path && data.path.startsWith(pkg.location),
pkg => data.path && isChildPath(pkg.location, data.path),
);
if (!pkg) {
callback();
+7 -3
View File
@@ -24,7 +24,7 @@ import webpack from 'webpack';
import nodeExternals from 'webpack-node-externals';
import { optimization } from './optimization';
import { Config } from '@backstage/config';
import { BundlingPaths } from './paths';
import { BundlingPaths, isChildPath } from './paths';
import { transforms } from './transforms';
import { LinkedPackageResolvePlugin } from './LinkedPackageResolvePlugin';
import { BundlingOptions, BackendBundlingOptions, LernaPackage } from './types';
@@ -87,7 +87,9 @@ export async function createConfig(
const { plugins, loaders } = transforms(options);
// Any package that is part of the monorepo but outside the monorepo root dir need
// separate resolution logic.
const externalPkgs = packages.filter(p => !p.location.startsWith(paths.root));
const externalPkgs = packages.filter(
p => !isChildPath(paths.root, p.location),
);
const baseUrl = frontendConfig.getString('app.baseUrl');
const validBaseUrl = new URL(baseUrl);
@@ -199,7 +201,9 @@ export async function createBackendConfig(
const moduleDirs = packages.map((p: any) =>
resolvePath(p.location, 'node_modules'),
);
const externalPkgs = packages.filter(p => !p.location.startsWith(paths.root)); // See frontend config
const externalPkgs = packages.filter(
p => !isChildPath(paths.root, p.location),
); // See frontend config
const { loaders } = transforms(options);
+17
View File
@@ -15,8 +15,25 @@
*/
import fs from 'fs-extra';
import path from 'path';
import { paths } from '../paths';
/**
* Checks if dir is the same as or a child of base.
*/
export function isChildPath(base: string, dir: string): boolean {
const relativePath = path.relative(base, dir);
if (relativePath === '') {
// The same directory
return true;
}
const outsideBase = relativePath.startsWith('..'); // not outside base
const differentDrive = path.isAbsolute(relativePath); // on Windows, this means dir is on a different drive from base.
return !outsideBase && !differentDrive;
}
export type BundlingPathsOptions = {
// bundle entrypoint, e.g. 'src/index'
entry: string;
+1 -1
View File
@@ -61,7 +61,7 @@ export async function serveBundle(options: ServeOptions) {
});
await new Promise<void>((resolve, reject) => {
server.listen(port, url.hostname, (err?: Error) => {
server.listen(port, host, (err?: Error) => {
if (err) {
reject(err);
return;
+21
View File
@@ -1,5 +1,26 @@
# @backstage/config-loader
## 0.4.1
### Patch Changes
- ad5c56fd9: Deprecate `$data` and replace it with `$include` which allows for any type of json value to be read from external files. In addition, `$include` can be used without a path, which causes the value at the root of the file to be loaded.
Most usages of `$data` can be directly replaced with `$include`, except if the referenced value is not a string, in which case the value needs to be changed. For example:
```yaml
# app-config.yaml
foo:
$data: foo.yaml#myValue # replacing with $include will turn the value into a number
$data: bar.yaml#myValue # replacing with $include is safe
# foo.yaml
myValue: 0xf00
# bar.yaml
myValue: bar
```
## 0.4.0
### Minor Changes
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
"version": "0.4.0",
"version": "0.4.1",
"private": false,
"publishConfig": {
"access": "public",
@@ -36,7 +36,7 @@
"fs-extra": "^9.0.0",
"json-schema": "^0.2.5",
"json-schema-merge-allof": "^0.7.0",
"typescript-json-schema": "^0.45.0",
"typescript-json-schema": "^0.47.0",
"yaml": "^1.9.2",
"yup": "^0.29.3"
},
@@ -28,6 +28,7 @@ const ctx: ReaderContext = {
'my-data.json': '{"a":{"b":{"c":42}}}',
'my-data.yaml': 'some:\n yaml:\n key: 7',
'my-data.yml': 'different: { key: hello }',
'invalid.yaml': 'foo: [}',
} as { [key: string]: string })[path];
if (!content) {
@@ -84,6 +85,41 @@ describe('readSecret', () => {
).rejects.toThrow('File not found!');
});
it('should include extra files', async () => {
// New format with path in fragment
await expect(
readSecret({ include: 'my-data.json#a.b.c' }, ctx),
).resolves.toBe(42);
await expect(
readSecret({ include: 'my-data.json#a.b' }, ctx),
).resolves.toEqual({ c: 42 });
await expect(
readSecret({ include: 'my-data.yaml#some.yaml.key' }, ctx),
).resolves.toBe(7);
await expect(readSecret({ include: 'my-data.yaml' }, ctx)).resolves.toEqual(
{
some: { yaml: { key: 7 } },
},
);
await expect(
readSecret({ include: 'my-data.yaml#' }, ctx),
).resolves.toEqual({
some: { yaml: { key: 7 } },
});
await expect(
readSecret({ include: 'my-data.yml#different.key' }, ctx),
).resolves.toBe('hello');
await expect(
readSecret({ include: 'no-data.yml#different.key' }, ctx),
).rejects.toThrow('File not found!');
await expect(
readSecret({ include: 'my-data.yml#missing.key' }, ctx),
).rejects.toThrow('Value is not an object at missing in my-data.yml');
await expect(readSecret({ include: 'invalid.yaml' }, ctx)).rejects.toThrow(
'Failed to parse included file invalid.yaml, YAMLSyntaxError: Flow sequence contains an unexpected }',
);
});
it('should reject invalid secrets', async () => {
await expect(readSecret('hello' as any, ctx)).rejects.toThrow(
'secret must be a `object` type, but the final value was: `"hello"`.',
+42 -2
View File
@@ -42,7 +42,12 @@ type DataSecret = {
path?: string;
};
type Secret = FileSecret | EnvSecret | DataSecret;
// TODO(Rugvip): Move this out of secret reading when we remove the deprecated DataSecret and $secret format
type IncludeSecret = {
include: string;
};
type Secret = FileSecret | EnvSecret | DataSecret | IncludeSecret;
// Schema for each type of secret description
const secretLoaderSchemas = {
@@ -55,6 +60,9 @@ const secretLoaderSchemas = {
data: yup.object({
data: yup.string().required(),
}),
include: yup.object({
include: yup.string().required(),
}),
};
// The top-level secret schema, which figures out what type of secret it is.
@@ -94,7 +102,7 @@ const dataSecretParser: {
export async function readSecret(
data: JsonObject,
ctx: ReaderContext,
): Promise<string | undefined> {
): Promise<JsonValue | undefined> {
const secret = secretSchema.validateSync(data, { strict: true }) as Secret;
if ('file' in secret) {
@@ -104,6 +112,9 @@ export async function readSecret(
return ctx.env[secret.env];
}
if ('data' in secret) {
console.warn(
`Configuration uses deprecated $data key, use $include instead.`,
);
const url =
'path' in secret ? `${secret.data}#${secret.path}` : secret.data;
const [filePath, dataPath] = url.split(/#(.*)/);
@@ -134,6 +145,35 @@ export async function readSecret(
return String(value);
}
if ('include' in secret) {
const [filePath, dataPath] = secret.include.split(/#(.*)/);
const ext = extname(filePath);
const parser = dataSecretParser[ext];
if (!parser) {
throw new Error(`No data secret parser available for extension ${ext}`);
}
const content = await ctx.readFile(filePath);
const parts = dataPath ? dataPath.split('.') : [];
let value: JsonValue | undefined;
try {
value = await parser(content);
} catch (error) {
throw new Error(`Failed to parse included file ${filePath}, ${error}`);
}
for (const [index, part] of parts.entries()) {
if (!isObject(value)) {
const errPath = parts.slice(0, index).join('.');
throw new Error(`Value is not an object at ${errPath} in ${filePath}`);
}
value = value[part];
}
return value;
}
isNever<typeof secret>();
throw new Error('Secret was left unhandled');
+2 -2
View File
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import { JsonObject } from '@backstage/config';
import { JsonObject, JsonValue } from '@backstage/config';
export type ReadFileFunc = (path: string) => Promise<string>;
export type ReadSecretFunc = (
path: string,
desc: JsonObject,
) => Promise<string | undefined>;
) => Promise<JsonValue | undefined>;
export type SkipFunc = (path: string) => boolean;
/**
+2 -2
View File
@@ -16,7 +16,7 @@
import fs from 'fs-extra';
import { resolve as resolvePath, dirname, isAbsolute } from 'path';
import { AppConfig, JsonObject } from '@backstage/config';
import { AppConfig, JsonObject, JsonValue } from '@backstage/config';
import { readConfigFile, readEnvConfig, readSecret } from './lib';
export type LoadConfigOptions = {
@@ -49,7 +49,7 @@ class Context {
async readSecret(
_path: string,
desc: JsonObject,
): Promise<string | undefined> {
): Promise<JsonValue | undefined> {
return readSecret(desc, this);
}
}
+23
View File
@@ -1,5 +1,28 @@
# @backstage/core-api
## 0.2.9
### Patch Changes
- ab0892358: Remove test dependencies from production package list
## 0.2.8
### Patch Changes
- a08c32ced: Add `FlatRoutes` component to replace the top-level `Routes` component from `react-router` within apps, removing the need for manually appending `/*` to paths or sorting routes.
- 86c3c652a: Deprecate `RouteRef` path parameter and member, and remove deprecated `routeRef.createSubRouteRef`.
- 27f2af935: Delay auth loginPopup close to avoid race condition with callers of authFlowHelpers.
## 0.2.7
### Patch Changes
- d681db2b5: Fix for GitHub and SAML auth not properly updating session state when already logged in.
- 1dc445e89: Introduce new plugin extension API
- Updated dependencies [1dc445e89]
- @backstage/test-utils@0.1.6
## 0.2.6
### Patch Changes
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-api",
"description": "Internal Core API used by Backstage plugins and apps",
"version": "0.2.6",
"version": "0.2.9",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,6 @@
},
"dependencies": {
"@backstage/config": "^0.1.2",
"@backstage/test-utils": "^0.1.5",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -43,7 +42,8 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.4.2",
"@backstage/cli": "^0.4.5",
"@backstage/test-utils": "^0.1.6",
"@backstage/test-utils-core": "^0.1.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
@@ -0,0 +1,76 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { ReactNode, Children, isValidElement, Fragment } from 'react';
import { useRoutes } from 'react-router-dom';
type RouteObject = {
path: string;
element: JSX.Element;
children?: RouteObject[];
};
// Similar to the same function from react-router, this collects routes from the
// children, but only the first level of routes
function createRoutesFromChildren(children: ReactNode): RouteObject[] {
return Children.toArray(children)
.flatMap(child => {
if (!isValidElement(child)) {
return [];
}
const { children } = child.props;
if (child.type === Fragment) {
return createRoutesFromChildren(children);
}
let path = child.props.path as string | undefined;
// TODO(Rugvip): Work around plugins registering empty paths, remove once deprecated routes are gone
if (path === '') {
return [];
}
path = path?.replace(/\/\*$/, '') ?? '/';
return [
{
path,
element: child,
children: children && [
{
path: '/*',
element: children,
},
],
},
];
})
.sort((a, b) => b.path.localeCompare(a.path))
.map(obj => {
obj.path = obj.path === '/' ? '/' : `${obj.path}/*`;
return obj;
});
}
type FlatRoutesProps = {
children: ReactNode;
};
export const FlatRoutes = (props: FlatRoutesProps): JSX.Element | null => {
const routes = createRoutesFromChildren(props.children);
return useRoutes(routes);
};
+3 -19
View File
@@ -25,25 +25,15 @@ export class AbsoluteRouteRef<Params extends { [param in string]: string }> {
// TODO(Rugvip): Remove this, routes are looked up via the registry instead
get path() {
return this.config.path;
return this.config.path ?? '';
}
get title() {
return this.config.title;
}
/**
* This function should not be used, create a separate RouteRef instead
* @deprecated
*/
createSubRoute(): any {
throw new Error(
'This method should not be called, create a separate RouteRef instead',
);
}
toString() {
return `routeRef{path=${this.path}}`;
return `routeRef{title=${this.title}}`;
}
}
@@ -54,13 +44,7 @@ export function createRouteRef<
return new AbsoluteRouteRef<Params>(config);
}
const create = Symbol('create-external-route-ref');
export class ExternalRouteRef {
static [create]() {
return new ExternalRouteRef();
}
private constructor() {}
toString() {
@@ -69,5 +53,5 @@ export class ExternalRouteRef {
}
export function createExternalRouteRef(): ExternalRouteRef {
return ExternalRouteRef[create]();
return new ((ExternalRouteRef as unknown) as { new (): ExternalRouteRef })();
}
+210 -44
View File
@@ -15,7 +15,11 @@
*/
import React, { PropsWithChildren } from 'react';
import { routePathCollector, routeParentCollector } from './collectors';
import {
routePathCollector,
routeParentCollector,
routeObjectCollector,
} from './collectors';
import {
traverseElementTree,
@@ -24,21 +28,22 @@ import {
} from '../extensions/traversal';
import { createRouteRef } from './RouteRef';
import { createPlugin } from '../plugin';
import { createRoutableExtension } from '../extensions';
import { attachComponentData, createRoutableExtension } from '../extensions';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { RouteRef } from './types';
const mockConfig = () => ({ path: '/foo', title: 'Foo' });
const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => (
<>{children}</>
);
const plugin = createPlugin({ id: 'my-plugin' });
const ref1 = createRouteRef(mockConfig());
const ref2 = createRouteRef(mockConfig());
const ref3 = createRouteRef(mockConfig());
const ref4 = createRouteRef(mockConfig());
const ref5 = createRouteRef(mockConfig());
const ref1 = createRouteRef({ path: '/foo1', title: 'Foo' });
const ref2 = createRouteRef({ path: '/foo2', title: 'Foo' });
const ref3 = createRouteRef({ path: '/foo3', title: 'Foo' });
const ref4 = createRouteRef({ path: '/foo4', title: 'Foo' });
const ref5 = createRouteRef({ path: '/foo5', title: 'Foo' });
const refOrder = [ref1, ref2, ref3, ref4, ref5];
const Extension1 = plugin.provide(
createRoutableExtension({
@@ -71,6 +76,30 @@ const Extension5 = plugin.provide(
}),
);
const AggregationComponent = ({
children,
}: PropsWithChildren<{
path: string;
}>) => <>{children}</>;
attachComponentData(AggregationComponent, 'core.gatherMountPoints', true);
function sortedEntries<T>(map: Map<RouteRef, T>): [RouteRef, T][] {
return Array.from(map).sort(
([a], [b]) => refOrder.indexOf(a) - refOrder.indexOf(b),
);
}
function routeObj(path: string, refs: RouteRef[], children: any[] = []) {
return {
path: path,
caseSensitive: false,
element: null,
routeRefs: new Set(refs),
children: children,
};
}
describe('discovery', () => {
it('should collect routes', () => {
const list = [
@@ -109,33 +138,40 @@ describe('discovery', () => {
</MemoryRouter>
);
const { routes, routeParents } = traverseElementTree({
const { routes, routeParents, routeObjects } = traverseElementTree({
root,
discoverers: [childDiscoverer, routeElementDiscoverer],
collectors: {
routes: routePathCollector,
routeParents: routeParentCollector,
routeObjects: routeObjectCollector,
},
});
expect(routes).toEqual(
new Map([
[ref1, '/foo'],
[ref2, '/bar/:id'],
[ref3, '/baz'],
[ref4, '/divsoup'],
[ref5, '/blop'],
]),
);
expect(routeParents).toEqual(
new Map([
[ref1, undefined],
[ref2, ref1],
[ref3, ref2],
[ref4, undefined],
[ref5, ref1],
]),
);
expect(sortedEntries(routes)).toEqual([
[ref1, '/foo'],
[ref2, '/bar/:id'],
[ref3, '/baz'],
[ref4, '/divsoup'],
[ref5, '/blop'],
]);
expect(sortedEntries(routeParents)).toEqual([
[ref1, undefined],
[ref2, ref1],
[ref3, ref2],
[ref4, undefined],
[ref5, ref1],
]);
expect(routeObjects).toEqual([
routeObj(
'/foo',
[ref1],
[
routeObj('/bar/:id', [ref2], [routeObj('/baz', [ref3])]),
routeObj('/blop', [ref5]),
],
),
routeObj('/divsoup', [ref4]),
]);
});
it('should handle all react router Route patterns', () => {
@@ -168,24 +204,154 @@ describe('discovery', () => {
routeParents: routeParentCollector,
},
});
expect(routes).toEqual(
new Map([
[ref1, '/foo'],
[ref2, '/bar/:id'],
[ref3, '/baz'],
[ref4, '/divsoup'],
[ref5, '/blop'],
]),
expect(sortedEntries(routes)).toEqual([
[ref1, '/foo'],
[ref2, '/bar/:id'],
[ref3, '/baz'],
[ref4, '/divsoup'],
[ref5, '/blop'],
]);
expect(sortedEntries(routeParents)).toEqual([
[ref1, undefined],
[ref2, ref1],
[ref3, undefined],
[ref4, ref3],
[ref5, ref3],
]);
});
it('should use the route aggregator key to bind child routes to the same path', () => {
const root = (
<MemoryRouter>
<Routes>
<AggregationComponent path="/foo">
<Extension1 />
<div>
<Extension2 />
</div>
HELLO
</AggregationComponent>
<Extension3 path="/bar">
<AggregationComponent path="/baz">
<Extension4>
<Extension5 />
</Extension4>
</AggregationComponent>
</Extension3>
</Routes>
</MemoryRouter>
);
expect(routeParents).toEqual(
new Map([
[ref1, undefined],
[ref2, ref1],
[ref3, undefined],
[ref4, ref3],
[ref5, ref3],
]),
const { routes, routeParents, routeObjects } = traverseElementTree({
root,
discoverers: [childDiscoverer, routeElementDiscoverer],
collectors: {
routes: routePathCollector,
routeParents: routeParentCollector,
routeObjects: routeObjectCollector,
},
});
expect(sortedEntries(routes)).toEqual([
[ref1, '/foo'],
[ref2, '/foo'],
[ref3, '/bar'],
[ref4, '/baz'],
[ref5, '/baz'],
]);
expect(sortedEntries(routeParents)).toEqual([
[ref1, undefined],
[ref2, undefined],
[ref3, undefined],
[ref4, ref3],
[ref5, ref3],
]);
expect(routeObjects).toEqual([
routeObj('/foo', [ref1, ref2]),
routeObj('/bar', [ref3], [routeObj('/baz', [ref4, ref5])]),
]);
});
it('should use the route aggregator but stop when encountering explicit path', () => {
const root = (
<MemoryRouter>
<Routes>
<Extension1 path="/foo">
<AggregationComponent path="/bar">
<Extension2>
<Extension3 path="/baz">
<Extension4 path="/blop" />
</Extension3>
<Extension5 />
</Extension2>
</AggregationComponent>
</Extension1>
</Routes>
</MemoryRouter>
);
const { routes, routeParents, routeObjects } = traverseElementTree({
root,
discoverers: [childDiscoverer, routeElementDiscoverer],
collectors: {
routes: routePathCollector,
routeParents: routeParentCollector,
routeObjects: routeObjectCollector,
},
});
expect(sortedEntries(routes)).toEqual([
[ref1, '/foo'],
[ref2, '/bar'],
[ref3, '/baz'],
[ref4, '/blop'],
[ref5, '/bar'],
]);
expect(sortedEntries(routeParents)).toEqual([
[ref1, undefined],
[ref2, ref1],
[ref3, ref1],
[ref4, ref3],
[ref5, ref1],
]);
expect(routeObjects).toEqual([
routeObj(
'/foo',
[ref1],
[
routeObj(
'/bar',
[ref2, ref5],
[routeObj('/baz', [ref3], [routeObj('/blop', [ref4])])],
),
],
),
]);
});
it('should stop gathering mount points after encountering explicit path', () => {
const root = (
<MemoryRouter>
<Routes>
<Extension1 path="/foo">
<AggregationComponent path="/bar">
<Extension2 path="/baz">
<Extension3 />
</Extension2>
</AggregationComponent>
</Extension1>
</Routes>
</MemoryRouter>
);
expect(() => {
traverseElementTree({
root,
discoverers: [childDiscoverer, routeElementDiscoverer],
collectors: {
routes: routePathCollector,
routeParents: routeParentCollector,
},
});
}).toThrow('Mounted routable extension must have a path');
});
it('should not visit the same element twice', () => {
+80 -17
View File
@@ -32,25 +32,48 @@ function getMountPoint(node: ReactElement): RouteRef | undefined {
export const routePathCollector = createCollector(
() => new Map<RouteRef, string>(),
(acc, node, parent) => {
(acc, node, parent, ctxPath: string | undefined) => {
// The context path is used during mount point gathering to assign the same path
// to all discovered mount points
let currentCtxPath = ctxPath;
if (parent?.props.element === node) {
return;
return currentCtxPath;
}
// Start gathering mount points when we encounter a mount point gathering flag
if (getComponentData<boolean>(node, 'core.gatherMountPoints')) {
const path: string | undefined = node.props?.path;
if (!path) {
throw new Error('Mount point gatherer must have a path');
}
currentCtxPath = path;
}
const routeRef = getMountPoint(node);
if (routeRef) {
const path: string | undefined = node.props?.path;
let path: string | undefined = node.props?.path;
// If we're gathering mount points we use the context path as out path, unless
// the element has its own path, in which case we use that instead and stop gathering
if (currentCtxPath) {
if (path) {
currentCtxPath = undefined;
} else {
path = currentCtxPath;
}
}
if (!path) {
throw new Error('Mounted routable extension must have a path');
}
acc.set(routeRef, path);
}
return currentCtxPath;
},
);
export const routeParentCollector = createCollector(
() => new Map<RouteRef, RouteRef | undefined>(),
(acc, node, parent, parentRouteRef?: RouteRef) => {
(acc, node, parent, parentRouteRef?: RouteRef | { sticky: RouteRef }) => {
if (parent?.props.element === node) {
return parentRouteRef;
}
@@ -59,8 +82,27 @@ export const routeParentCollector = createCollector(
const routeRef = getMountPoint(node);
if (routeRef) {
acc.set(routeRef, parentRouteRef);
nextParent = routeRef;
// "sticky" route ref is when we've encountered a mount point gatherer, and we want a
// mount points beneath it to have the same parent, regardless of internal structure
if (parentRouteRef && 'sticky' in parentRouteRef) {
acc.set(routeRef, parentRouteRef.sticky);
// When we encounter a mount point with an explicit path, we stop gathering
// mount points withing the children and remove the sticky state
if (node.props?.path) {
nextParent = routeRef;
} else {
nextParent = parentRouteRef;
}
} else {
acc.set(routeRef, parentRouteRef);
nextParent = routeRef;
}
}
// Mount point gatherers are marked as "sticky"
if (getComponentData<boolean>(node, 'core.gatherMountPoints')) {
return { sticky: nextParent };
}
return nextParent;
@@ -69,9 +111,10 @@ export const routeParentCollector = createCollector(
export const routeObjectCollector = createCollector(
() => Array<BackstageRouteObject>(),
(acc, node, parent, parentChildArr: BackstageRouteObject[] = acc) => {
(acc, node, parent, parentObj: BackstageRouteObject | undefined) => {
const parentChildren = parentObj?.children ?? acc;
if (parent?.props.element === node) {
return parentChildArr;
return parentObj;
}
const path: string | undefined = node.props?.path;
@@ -79,20 +122,40 @@ export const routeObjectCollector = createCollector(
const routeRef = getMountPoint(node);
if (routeRef) {
const children: BackstageRouteObject[] = [];
if (!path) {
throw new Error(`No path found for mount point ${routeRef}`);
if (path) {
const newObject: BackstageRouteObject = {
caseSensitive,
path,
element: null,
routeRefs: new Set([routeRef]),
children: [],
};
parentChildren.push(newObject);
return newObject;
}
parentChildArr.push({
parentObj?.routeRefs.add(routeRef);
}
const isGatherer = getComponentData<boolean>(
node,
'core.gatherMountPoints',
);
if (isGatherer) {
if (!path) {
throw new Error('Mount point gatherer must have a path');
}
const newObject: BackstageRouteObject = {
caseSensitive,
path,
element: null,
routeRef,
children,
});
return children;
routeRefs: new Set(),
children: [],
};
parentChildren.push(newObject);
return newObject;
}
return parentChildArr;
return parentObj;
},
);
+2 -2
View File
@@ -60,8 +60,8 @@ class RouteResolver {
currentRouteRef;
currentRouteRef = this.routeParents.get(currentRouteRef)
) {
matchIndex = match.findIndex(
m => (m.route as BackstageRouteObject).routeRef === currentRouteRef,
matchIndex = match.findIndex(m =>
(m.route as BackstageRouteObject).routeRefs.has(currentRouteRef!),
);
if (matchIndex !== -1) {
break;
+1
View File
@@ -21,5 +21,6 @@ export type {
ConcreteRoute,
MutableRouteRef,
} from './types';
export { FlatRoutes } from './FlatRoutes';
export { createRouteRef } from './RouteRef';
export { useRouteRef } from './hooks';
+4 -7
View File
@@ -19,14 +19,10 @@ import { IconComponent } from '../icons';
// @ts-ignore, we're just embedding the Params type for usage in other places
export type RouteRef<Params extends { [param in string]: string } = {}> = {
// TODO(Rugvip): Remove path, look up via registry instead
/** @deprecated paths are no longer accessed directly from RouteRefs, use useRouteRef instead */
path: string;
icon?: IconComponent;
title: string;
/**
* This function should not be used, create a separate RouteRef instead
* @deprecated
*/
createSubRoute(): any;
};
export type AnyRouteRef = RouteRef<any>;
@@ -51,7 +47,8 @@ export type MutableRouteRef = RouteRef<{}>;
export type RouteRefConfig<Params extends { [param in string]: string }> = {
params?: Array<keyof Params>;
path: string;
/** @deprecated Route refs no longer decide their own path */
path?: string;
icon?: IconComponent;
title: string;
};
@@ -62,5 +59,5 @@ export interface BackstageRouteObject {
children?: BackstageRouteObject[];
element: React.ReactNode;
path: string;
routeRef: AnyRouteRef;
routeRefs: Set<AnyRouteRef>;
}
+20
View File
@@ -1,5 +1,25 @@
# @backstage/core
## 0.4.3
### Patch Changes
- a08c32ced: Add `FlatRoutes` component to replace the top-level `Routes` component from `react-router` within apps, removing the need for manually appending `/*` to paths or sorting routes.
- Updated dependencies [a08c32ced]
- Updated dependencies [86c3c652a]
- Updated dependencies [27f2af935]
- @backstage/core-api@0.2.8
## 0.4.2
### Patch Changes
- 1dc445e89: Update to use new plugin extension API
- 342270e4d: Create AboutCard in core and use it in pagerduty and catalog plugin
- Updated dependencies [d681db2b5]
- Updated dependencies [1dc445e89]
- @backstage/core-api@0.2.7
## 0.4.1
### Patch Changes
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/core",
"description": "Core API used by Backstage plugins and apps",
"version": "0.4.1",
"version": "0.4.3",
"private": false,
"publishConfig": {
"access": "public",
@@ -30,7 +30,7 @@
},
"dependencies": {
"@backstage/config": "^0.1.2",
"@backstage/core-api": "^0.2.6",
"@backstage/core-api": "^0.2.8",
"@backstage/theme": "^0.2.2",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -65,8 +65,8 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/cli": "^0.4.2",
"@backstage/test-utils": "^0.1.5",
"@backstage/cli": "^0.4.4",
"@backstage/test-utils": "^0.1.6",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
"@testing-library/user-event": "^12.0.7",
@@ -14,8 +14,7 @@
* limitations under the License.
*/
import React from 'react';
import { VerticalIcon } from './VerticalIcon';
import { SubHeaderLink } from '../types';
import { IconLinkVertical, IconLinkVerticalProps } from './IconLinkVertical';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
@@ -29,21 +28,15 @@ const useStyles = makeStyles(theme => ({
}));
type Props = {
links: SubHeaderLink[];
links: IconLinkVerticalProps[];
};
export const SubHeader = ({ links }: Props) => {
export const HeaderIconLinkRow = ({ links }: Props) => {
const classes = useStyles();
return (
<nav className={classes.links}>
{links.map(link => (
<VerticalIcon
key={link.title}
label={link.title}
href={link.href ?? '#'}
action={link.action}
icon={link.icon}
/>
{links.map((link, index) => (
<IconLinkVertical key={index + 1} {...link} />
))}
</nav>
);
@@ -13,21 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as React from 'react';
import React from 'react';
import classnames from 'classnames';
import { makeStyles, Link } from '@material-ui/core';
import LinkIcon from '@material-ui/icons/Link';
import { Link as RouterLink } from 'react-router-dom';
import { Link as RouterLink } from '../Link';
export type IconLinkVerticalProps = {
icon?: React.ReactNode;
href?: string;
disabled?: boolean;
title?: string;
label: string;
action?: React.ReactNode;
};
const useIconStyles = makeStyles({
const useIconStyles = makeStyles(theme => ({
link: {
display: 'grid',
justifyItems: 'center',
@@ -43,12 +43,16 @@ const useIconStyles = makeStyles({
fontWeight: 600,
letterSpacing: 1.2,
},
});
linkStyle: {
color: theme.palette.secondary.main,
},
}));
export function IconLinkVertical({
icon = <LinkIcon />,
href = '#',
disabled = false,
action,
...props
}: IconLinkVerticalProps) {
const classes = useIconStyles();
@@ -58,7 +62,6 @@ export function IconLinkVertical({
<Link
className={classnames(classes.link, classes.disabled)}
underline="none"
title={props.title}
{...props}
>
{icon}
@@ -67,12 +70,11 @@ export function IconLinkVertical({
);
}
// Absolute links should not be using RouterLink
if (href?.startsWith('//') || href?.includes('://')) {
if (action) {
return (
<Link className={classes.link} href={href} {...props}>
<Link className={classnames(classes.link, classes.linkStyle)} {...props}>
{icon}
<span className={classes.label}>{props.label}</span>
{action}
</Link>
);
}

Some files were not shown because too many files have changed in this diff Show More