Merge branch 'master' into 3965-breadcrumbs

This commit is contained in:
tudi2d
2021-02-12 12:01:33 +01:00
384 changed files with 12216 additions and 3743 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
pass registered logger to requestLoggingHandler
-29
View File
@@ -1,29 +0,0 @@
---
'@backstage/create-app': patch
---
Pass on plugin database management instance that is now required by the scaffolder plugin.
To apply this change to an existing application, add the following to `src/plugins/scaffolder.ts`:
```diff
export default async function createPlugin({
logger,
config,
+ database,
}: PluginEnvironment) {
// ...omitted...
return await createRouter({
preparers,
templaters,
publishers,
logger,
config,
dockerClient,
entityClient,
+ database,
});
}
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/integration': minor
---
Make `ScmIntegration.resolveUrl` mandatory.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-circleci': patch
---
Migrated to new composability API, exporting the plugin instance as `circleCIPlugin`, the entity page content as `EntityCircleCIContent`, and entity conditional as `isCircleCIAvailable`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-search': minor
---
Migrated to new composability API, exporting the plugin instance as `searchPlugin`, and page as `SearchPage`. Due to the old router component also being called `SearchPage`, this is a breaking change. The old page component is now exported as `Router`, which can be used to maintain the old behavior.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/backend-common': patch
---
Implement `UrlReader.search` for the other providers (Azure, Bitbucket, GitLab) as well.
The `UrlReader` subclasses now are implemented in terms of the respective `Integration` class.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-backend': patch
---
Don't respond to a request twice if an entity has not been found.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-cloudbuild': patch
---
Migrate to new composability API, exporting the plugin instance as `cloudbuildPlugin`, the entity content as `EntityCloudbuildContent`, the entity conditional as `isCloudbuildAvailable`, and entity cards as `EntityLatestCloudbuildRunCard` and `EntityLatestCloudbuildsForBranchCard`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-register-component': patch
---
Migrated to new composability API, exporting the plugin instance as `registerComponentPlugin`, and page as `RegisterComponentPage`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-user-settings': patch
---
Use routed tabs to link to every settings page.
-7
View File
@@ -1,7 +0,0 @@
---
'@backstage/plugin-scaffolder-backend': minor
---
Introduced `v2` Scaffolder REST API, which uses an implementation that is database backed, making the scaffolder instances stateless. The `createRouter` function now requires a `PluginDatabaseManager` instance to be passed in, commonly available as `database` in the plugin environment in the backend.
This API should be considered unstable until used by the scaffolder frontend.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Tweak error message in lockfile parsing to include more information.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-techdocs': patch
---
Migrated to new composability API, exporting the plugin instance as `techdocsPlugin`, the top-level page as `TechdocsPage`, and the entity content as `EntityTechdocsContent`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Limit the props that are forwarded to the `Link` component in the `EntityRefLink`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/backend-common': patch
---
Slight refactoring in support of a future search implementation in `UrlReader`. Mostly moving code around.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-pagerduty': patch
---
Migrated to new composability API, exporting the plugin instance as `pagerDutyPlugin`, entity card as `EntityPagerDutyCard`, and entity conditional as `isPagerDutyAvailable`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/test-utils': patch
---
Added `mountedRoutes` option to `wrapInTestApp`, allowing routes to be associated to concrete paths to make `useRouteRef` usable in tested components.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-jenkins': patch
---
Migrate to new composability API, exporting the plugin instance as `jenkinsPlugin`, the entity content as `EntityJenkinsContent`, the entity conditional as `isJenkinsAvailable`, and the entity card as `EntityLatestJenkinsRunCard`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-github-actions': patch
---
Migrate to new composability API, exporting the plugin instance as `githubActionsPlugin`, the entity content as `EntityGithubActionsContent`, entity conditional as `isGithubActionsAvailable`, and entity cards as `EntityLatestGithubActionRunCard`, `EntityLatestGithubActionsForBranchCard`, and `EntityRecentGithubActionsRunsCard`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-newrelic': patch
---
Migrate to new composability API, exporting the plugin instance as `newRelicPlugin`, and the root page as `NewRelicPage`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-kubernetes': patch
---
Migrate to new composability API, exporting the plugin instance as `kubernetesPlugin` and entity content as `EntityKubernetesContent`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Migrate about card to new composability API, exporting the entity cards as `EntityAboutCard`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-kafka': patch
---
Migrate to new composability API, exporting the plugin instance as `kafkaPlugin`, entity content as `EntityKafkaContent`, and entity conditional as `isKafkaAvailable`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Migrated the package from using moment to Luxon. #4278
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core': patch
---
Export Select component
@@ -1,5 +0,0 @@
---
'@backstage/plugin-techdocs': patch
---
Enhance API calls to support trapping 500 errors from techdocs-backend
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core': patch
---
Make sure that SidebarItems are also active when on sub route.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Fix parsing of the path to default to empty string not undefined if git-url-parse throws something we don't expect. Fixes the error `The "path" argument must be of type string.` when preparing.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/core': patch
---
Fixed type inference of `createRouteRef`.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-gitops-profiles': patch
---
Migrated to new composability API, exporting the plugin instance as `gitopsProfilesPlugin` and pages as `GitopsProfilesClusterListPage`, `GitopsProfilesClusterPage`, and `GitopsProfilesCreatePage`.
+1
View File
@@ -7,6 +7,7 @@
* @backstage/maintainers
/docs/features/techdocs @backstage/techdocs-core
/docs/features/search @backstage/techdocs-core
/docs/assets/search @backstage/techdocs-core
/plugins/cost-insights @backstage/silver-lining
/plugins/cloudbuild @trivago/ebarrios
/plugins/search @backstage/techdocs-core
+2
View File
@@ -119,6 +119,7 @@ Kumar
learnings
lerna
Lerna
lockfile
Luxon
magiclink
mailto
@@ -249,6 +250,7 @@ transpilation
Tuite
ui
unmanaged
unregister
untracked
upvote
url
+1 -1
View File
@@ -77,7 +77,7 @@ jobs:
run: yarn prettier:check
- name: validate config
run: yarn backstage-cli config:check
run: yarn backstage-cli config:check --lax
- name: lint
run: yarn lerna -- run lint --since origin/master
+1 -1
View File
@@ -65,7 +65,7 @@ jobs:
COMMIT_SHA_BEFORE: '${{ github.event.before }}'
- name: validate config
run: yarn backstage-cli config:check
run: yarn backstage-cli config:check --lax
- name: lint
run: yarn lerna -- run lint
+24 -4
View File
@@ -48,11 +48,14 @@ jobs:
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
});
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
- name: yarn install
run: yarn --cwd cypress install
# This is required because the environment_url param that Tugboat uses
# to tell us where the preview is located isn't supported unless you
# specify the custom Accept header when getting the deployment_status,
@@ -77,9 +80,25 @@ jobs:
});
console.log(result);
return result.data.environment_url;
- name: echo tugboat preview url
run: |
curl ${{steps.get-status-env.outputs.result}}
- name: cypress run
uses: cypress-io/github-action@v2
env:
CYPRESS_baseUrl: ${{steps.get-status-env.outputs.result}}
with:
config-file: ./cypress.json
working-directory: ./cypress
browser: chrome
install: false
headless: true
- name: update artifact
if: ${{ always() }}
uses: actions/upload-artifact@v1
with:
name: cypress-videos
path: ./cypress/cypress/videos
- name: set status
if: ${{ failure() }}
uses: actions/github-script@v3
@@ -94,6 +113,7 @@ jobs:
context: 'Backstage Tugboat E2E Tests',
target_url: "https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}"
});
- name: set status
if: ${{ success() }}
uses: actions/github-script@v3
+1
View File
@@ -53,6 +53,7 @@ Check out [the documentation](https://backstage.io/docs/getting-started) on how
- [Adopters](ADOPTERS.md) - Companies already using Backstage
- [Blog](https://backstage.io/blog/) - Announcements and updates
- [Newsletter](https://mailchi.mp/spotify/backstage-community) - Subscribe to our email newsletter
- [Backstage Community Sessions](https://github.com/backstage/community) - Join monthly meetup and explore Backstage community
- Give us a star ⭐️ - If you are using Backstage or think it is an interesting project, we would love a star ❤️
## License
+7
View File
@@ -73,6 +73,9 @@ organization:
name: My Company
# Reference documentation http://backstage.io/docs/features/techdocs/configuration
# Note: After experimenting with basic setup, use CI/CD to generate docs
# and an external cloud storage when deploying TechDocs for production use-case.
# https://backstage.io/docs/features/techdocs/how-to-guides#how-to-migrate-from-techdocs-basic-to-recommended-deployment-approach
techdocs:
builder: 'local' # Alternatives - 'external'
generators:
@@ -305,6 +308,10 @@ auth:
$env: AUTH_OAUTH2_AUTH_URL
tokenUrl:
$env: AUTH_OAUTH2_TOKEN_URL
###
# provide a list of scopes as needed for your OAuth2 Server:
#
# scope: saml-login-selector openid profile email
oidc:
development:
metadataUrl:
+9
View File
@@ -4,6 +4,15 @@ metadata:
name: backstage
description: |
Backstage is an open-source developer portal that puts the developer experience first.
links:
- title: Website
url: http://backstage.io
- title: Documentation
url: https://backstage.io/docs
- title: Storybook
url: https://backstage.io/storybook
- title: Discord Chat
url: https://discord.com/invite/EBHEGzX
annotations:
github.com/project-slug: backstage/backstage
backstage.io/techdocs-ref: url:https://github.com/backstage/backstage
+17
View File
@@ -0,0 +1,17 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint.backend')],
rules: {
'no-console': 0,
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: true,
optionalDependencies: false,
peerDependencies: false,
bundledDependencies: false,
},
],
'jest/expect-expect': 'off',
'no-restricted-syntax': 'off',
},
};
+27
View File
@@ -0,0 +1,27 @@
# Cypress Tests for Backstage
Hey 👋 Welcome to the Cypress tests for Backstage. They're designed to be run against the `packages/app` folder in the main repo, and be some form of smoke tests to make sure that we don't break any core functionality.
They run part of the PR build, and are triggered from the `.github/workflows/tugboat.yml` file.
The main app gets built up part of a [Tugboat Build](https://tugboat.qa), which when complete, sends a `deployment event` to the PR triggering the aforementioned workflow.
### Running Locally
In order to make typescript happy, this `cypress` package is separate from all the Jest dependencies in the monorepo workspaces setup.
You can run the e2e tests locally pointing at your local running version like so:
```sh
cd cypress
yarn install
yarn cypress run
```
You can open up the `cypress` console by using `yarn cypress open`.
You can also run towards any Backstage installation by using the Cypress Environment Variable overrides.
```sh
CYPRESS_baseUrl="http://demo.backstage.io" yarn cypress run
```
+8
View File
@@ -0,0 +1,8 @@
{
"baseUrl": "http://localhost:7000",
"integrationFolder": "./src/integration",
"supportFile": "./src/support",
"fixturesFolder": "./src/fixures",
"pluginsFile": "./src/plugins",
"defaultCommandTimeout": 10000
}
+11
View File
@@ -0,0 +1,11 @@
{
"name": "@backstage/cypress-tests",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"private": true,
"dependencies": {
"cypress": "^6.4.0",
"typescript": "^4.1.3"
}
}
View File
+29
View File
@@ -0,0 +1,29 @@
/*
* Copyright 2021 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.
*/
/// <reference types="cypress" />
import 'os';
describe('Catalog', () => {
describe('default entities', () => {
it('displays the correct amount of entities from default config', () => {
cy.loginAsGuest();
cy.visit('/catalog');
cy.contains('Owned (7)').should('be.visible');
});
});
});
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright 2021 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.
*/
export default () => {};
+25
View File
@@ -0,0 +1,25 @@
/*
* Copyright 2021 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.
*/
/// <reference types="cypress" />
Cypress.Commands.add('loginAsGuest', () => {
cy.visit('/', {
onLoad: (win: Window) =>
win.localStorage.setItem('@backstage/core:SignInPage:provider', 'guest'),
});
});
export {};
@@ -13,19 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import parseGitUrl from 'git-url-parse';
export type UrlType = 'file' | 'tree';
export function urlType(url: string): UrlType {
const { filepathtype, filepath } = parseGitUrl(url);
if (filepathtype === 'tree' || filepathtype === 'file') {
return filepathtype;
} else if (filepath?.match(/\.ya?ml$/)) {
return 'file';
declare module 'zombie';
declare module 'pgtools';
declare namespace Cypress {
interface Chainable {
/**
* Login as guest
* @example cy.loginAsGuests
*/
loginAsGuest(): Chainable<Element>;
}
return 'tree';
}
+37
View File
@@ -0,0 +1,37 @@
{
"compilerOptions": {
"allowJs": true,
"declaration": true,
"declarationMap": false,
"esModuleInterop": true,
"experimentalDecorators": false,
"forceConsistentCasingInFileNames": true,
"importHelpers": false,
"incremental": true,
"isolatedModules": true,
"jsx": "react",
"lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2020", "ESNext.Promise"],
"module": "ESNext",
"moduleResolution": "node",
"noEmit": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"pretty": true,
"removeComments": false,
"resolveJsonModule": true,
"sourceMap": false,
"skipLibCheck": false,
"strict": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"stripInternal": true,
"target": "ES2019",
"types": ["node", "cypress"]
}
}
+1409
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -119,7 +119,7 @@ Plugins supply their APIs through the `apis` option of `createPlugin`, for
example:
```ts
export const plugin = createPlugin({
export const techdocsPlugin = createPlugin({
id: 'techdocs',
apis: [
createApiFactory({
+45 -26
View File
@@ -1,13 +1,13 @@
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="789px" height="766px" viewBox="-0.5 -0.5 789 766" content="&lt;mxfile host=&quot;bd2205bb-07f8-4b61-b1c1-5174fe4ebe37&quot; modified=&quot;2021-01-14T13:46:42.842Z&quot; agent=&quot;5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.52.0 Chrome/83.0.4103.122 Electron/9.3.5 Safari/537.36&quot; etag=&quot;jW9IV2PM6529z9FP4d6-&quot; version=&quot;13.10.0&quot; type=&quot;embed&quot;&gt;&lt;diagram id=&quot;AOZgdlUmH_6GT6Gt5u4e&quot; name=&quot;Page-1&quot;&gt;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&lt;/diagram&gt;&lt;/mxfile&gt;" style="background-color: rgb(255, 255, 255);">
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="789px" height="766px" viewBox="-0.5 -0.5 789 766" content="&lt;mxfile host=&quot;be2f1433-9da5-431b-92db-92882262d7d1&quot; modified=&quot;2021-02-10T13:37:29.724Z&quot; agent=&quot;5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.53.0 Chrome/87.0.4280.141 Electron/11.2.1 Safari/537.36&quot; etag=&quot;EtMH9Eo_7GPdazvr9TBW&quot; version=&quot;14.2.4&quot; type=&quot;embed&quot;&gt;&lt;diagram id=&quot;AOZgdlUmH_6GT6Gt5u4e&quot; name=&quot;Page-1&quot;&gt;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&lt;/diagram&gt;&lt;/mxfile&gt;" style="background-color: rgb(255, 255, 255);">
<defs/>
<g>
<rect x="560" y="484" width="140" height="140" fill="#21c0a5" stroke="#006658" pointer-events="all"/>
<rect x="420" y="484" width="140" height="140" fill="#21c0a5" stroke="#006658" pointer-events="all"/>
<path d="M 664.5 595 L 664.5 705 L 595.54 705" fill="none" stroke="#006658" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 590.29 705 L 597.29 701.5 L 595.54 705 L 597.29 708.5 Z" fill="#006658" stroke="#006658" stroke-miterlimit="10" pointer-events="all"/>
<rect x="420" y="110" width="140" height="140" fill="#21c0a5" stroke="#006658" pointer-events="all"/>
<rect x="420" y="110" width="135" height="140" fill="#21c0a5" stroke="#006658" pointer-events="all"/>
<rect x="420" y="299" width="280" height="140" fill="#21c0a5" stroke="#006658" pointer-events="all"/>
<rect x="560" y="110" width="140" height="140" fill="#21c0a5" stroke="#006658" pointer-events="all"/>
<rect x="565" y="110" width="135" height="140" fill="#21c0a5" stroke="#006658" pointer-events="all"/>
<rect x="90" y="469.25" width="110" height="90" fill="#21c0a5" stroke="#006658" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
@@ -66,24 +66,26 @@
</text>
</switch>
</g>
<rect x="415" y="90" width="210" height="20" fill="none" stroke="none" pointer-events="all"/>
<rect x="419.59" y="80" width="140" height="30" 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: 100px; margin-left: 520px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe flex-start; width: 1px; height: 1px; padding-top: 95px; margin-left: 422px;">
<div style="box-sizing: border-box; font-size: 0; text-align: left; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #5C5C5C; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
@backstage/plugin-search-backend
@backstage/
<br/>
plugin-search-backend
</div>
</div>
</div>
</foreignObject>
<text x="520" y="104" fill="#5C5C5C" font-family="Helvetica" font-size="12px" text-anchor="middle">
@backstage/plugin-search-backend
<text x="422" y="99" fill="#5C5C5C" font-family="Helvetica" font-size="12px">
@backstage/...
</text>
</switch>
</g>
<rect x="90" y="433.75" width="160" height="30" fill="none" stroke="none" pointer-events="all"/>
<rect x="90" y="433.75" width="150" height="30" 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">
@@ -138,9 +140,9 @@
</text>
</switch>
</g>
<path d="M 630 364.25 L 758 364.3 L 758 177.3 L 661.87 177.25" fill="none" stroke="#006658" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 656.62 177.25 L 663.62 173.75 L 661.87 177.25 L 663.62 180.75 Z" fill="#006658" stroke="#006658" stroke-miterlimit="10" pointer-events="all"/>
<rect x="415" y="279" width="280" height="20" fill="none" stroke="none" pointer-events="all"/>
<path d="M 636.37 364.25 L 758 364.3 L 758 177.3 L 655.5 177.25" fill="none" stroke="#006658" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 631.12 364.25 L 638.12 360.75 L 636.37 364.25 L 638.12 367.75 Z" fill="#006658" stroke="#006658" stroke-miterlimit="10" pointer-events="all"/>
<rect x="420" y="279" width="270" 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">
@@ -266,7 +268,7 @@
<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: 220px; margin-left: 351px;">
<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: 220px; margin-left: 350px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #5C5C5C; line-height: 1.2; pointer-events: all; background-color: #ffffff; white-space: nowrap; ">
Pass Search
@@ -280,7 +282,7 @@
</div>
</div>
</foreignObject>
<text x="351" y="223" fill="#5C5C5C" font-family="Helvetica" font-size="11px" text-anchor="middle">
<text x="350" y="223" fill="#5C5C5C" font-family="Helvetica" font-size="11px" text-anchor="middle">
Pass Search...
</text>
</switch>
@@ -352,8 +354,8 @@
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 49px; height: 1px; padding-top: 165px; margin-left: 606px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #FFFFFF; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
<font style="font-size: 9px">
Gather Documents
<font style="font-size: 7px">
Gather Documents From Plugins
</font>
</div>
</div>
@@ -366,7 +368,7 @@
</g>
<path d="M 444.17 349.5 C 444.17 341.22 453.31 334.5 464.59 334.5 C 470.01 334.5 475.2 336.08 479.03 338.89 C 482.86 341.71 485.01 345.52 485.01 349.5 L 485.01 379 C 485.01 387.28 475.87 394 464.59 394 C 453.31 394 444.17 387.28 444.17 379 Z" fill="#21c0a5" stroke="#006658" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 485.01 349.5 C 485.01 357.78 475.87 364.5 464.59 364.5 C 453.31 364.5 444.17 357.78 444.17 349.5" fill="none" stroke="#006658" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 550 364.25 L 517.5 364.3 L 485.01 364.3" fill="none" stroke="#006658" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 550 364.25 L 517.5 364.3 L 485.01 364.25" fill="none" stroke="#006658" stroke-miterlimit="10" pointer-events="stroke"/>
<rect x="550" y="324.25" width="80" height="80" fill="#21c0a5" stroke="#006658" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
@@ -375,16 +377,14 @@
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #FFFFFF; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
<font style="font-size: 11px">
Collate Documents
<br/>
Or Metadata
Register Document / Metadata Collation Handler(s)
</font>
</div>
</div>
</div>
</foreignObject>
<text x="590" y="368" fill="#FFFFFF" font-family="Helvetica" font-size="12px" text-anchor="middle">
Collate Docum...
Register Docu...
</text>
</switch>
</g>
@@ -427,11 +427,11 @@
</text>
</switch>
</g>
<rect x="560" y="110" width="80" height="20" fill="none" stroke="none" pointer-events="all"/>
<rect x="565" y="110" width="80" 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: 1px; height: 1px; padding-top: 120px; margin-left: 600px;">
<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: 120px; margin-left: 605px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #FFFFFF; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
<font style="font-size: 9px">
@@ -441,7 +441,7 @@
</div>
</div>
</foreignObject>
<text x="600" y="123" fill="#FFFFFF" font-family="Helvetica" font-size="11px" text-anchor="middle">
<text x="605" y="123" fill="#FFFFFF" font-family="Helvetica" font-size="11px" text-anchor="middle">
Index Processi...
</text>
</switch>
@@ -529,10 +529,29 @@
</text>
</switch>
</g>
<rect x="565" y="80" width="130" height="30" 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 flex-start; width: 1px; height: 1px; padding-top: 95px; margin-left: 567px;">
<div style="box-sizing: border-box; font-size: 0; text-align: left; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #5C5C5C; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
@backstage/
<br/>
plugin-search-indexer
</div>
</div>
</div>
</foreignObject>
<text x="567" y="99" fill="#5C5C5C" font-family="Helvetica" font-size="12px">
@backstage/...
</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">
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
Viewer does not support full SVG 1.1
</text>

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 46 KiB

+22 -8
View File
@@ -76,14 +76,16 @@ by also providing the `cert` configuration.
### Configuration
Each authentication provider (except SAML) needs five parameters: an OAuth
client ID, a client secret, an authorization endpoint, a token endpoint, and an
app origin. The app origin is the URL at which the frontend of the application
is hosted, and it is read from the `app.baseUrl` config. This is required
because the application opens a popup window to perform the authentication, and
once the flow is completed, the popup window sends a `postMessage` to the
frontend application to indicate the result of the operation. Also this URL is
used to verify that authentication requests are coming from only this endpoint.
Each authentication provider (except SAML) needs six parameters: an OAuth client
ID, a client secret, an authorization endpoint, a token endpoint, an optional
list of scopes (as a string separated by spaces) that may be required by the
OAuth2 Server to enable end-user sign-on, and an app origin. The app origin is
the URL at which the frontend of the application is hosted, and it is read from
the `app.baseUrl` config. This is required because the application opens a popup
window to perform the authentication, and once the flow is completed, the popup
window sends a `postMessage` to the frontend application to indicate the result
of the operation. Also this URL is used to verify that authentication requests
are coming from only this endpoint.
These values are configured via the `app-config.yaml` present in the root of
your app folder.
@@ -109,6 +111,18 @@ auth:
development:
clientId:
$env:
oauth2:
development:
clientId:
$env: AUTH_OAUTH2_CLIENT_ID
clientSecret:
$env: AUTH_OAUTH2_CLIENT_SECRET
authorizationUrl:
$env: AUTH_OAUTH2_AUTH_URL
tokenUrl:
$env: AUTH_OAUTH2_TOKEN_URL
scope:
$env: AUTH_OAUTH2_SCOPE
saml:
entryPoint:
$env: AUTH_SAML_ENTRY_POINT
+12 -6
View File
@@ -24,12 +24,18 @@ Backstage ecosystem.
## Project roadmap
| Version | Description |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Backstage Search V.0 ✅ | Search Frontend letting you search through the entities of the software catalog. [See V.0 Use Cases.](#backstage-search-v0) |
| Backstage Search V.1 ⌛ | Basic “out-of-the-box” in-memory indexing process of entities, and their metadata, registered to the Software Catalog. [See V.1 Use Cases.](#backstage-search-v1) |
| Backstage Search V.2 ⌛ | Search Backend responsible for the indexing process of entities, and their metadata, registered to the Software Catalog. [See V.2 Use Cases.](#backstage-search-v2) |
| Backstage Search V.3 ⌛ | Standardized Search API lets you index other plugins data to the search engine of choice. [See V.3 Use Cases.](#backstage-search-v3) |
| Version | Description |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Backstage Search v0 ✅ | Search Frontend letting you search through the entities of the software catalog. [See v0 Use Cases.](#backstage-search-v0) |
| [Backstage Search V0.5 ⌛][v0.5] | Foundations for the architecture. |
| [Backstage Search v1 ⌛][v1] | Basic “out-of-the-box” in-memory indexing process of entities, and their metadata, registered to the Software Catalog. [See v1 Use Cases.](#backstage-search-v1) |
| [Backstage Search v2 ⌛][v2] | Search Backend responsible for the indexing process of entities, and their metadata, registered to the Software Catalog. [See v2 Use Cases.](#backstage-search-v2) |
| [Backstage Search v3 ⌛][v3] | Standardized Search API lets you index other plugins data to the search engine of choice. [See v3 Use Cases.](#backstage-search-v3) |
[v0.5]: https://github.com/backstage/backstage/milestone/25
[v1]: https://github.com/backstage/backstage/milestone/26
[v2]: https://github.com/backstage/backstage/milestone/27
[v3]: https://github.com/backstage/backstage/milestone/28
## Use Cases
+19 -10
View File
@@ -6,9 +6,9 @@ description: Documentation on Search Architecture
# Search Architecture
> _This is a proposed architecture which has not been implemented yet. We are
> still looking for feedback to improve the architecture to fit your use-case,
> see [this open issue](https://github.com/backstage/backstage/issues/4078)._
> _This is a proposed architecture which has not been implemented yet. Find our
> milestones to follow our progress on the
> [Search Roadmap](./README.md#project-roadmap)._
Below you can explore the Search Architecture. Our aim with this architecture is
to support a wide variety of search engines, while providing a simple developer
@@ -20,7 +20,10 @@ Backstage end-users.
At a base-level, we want to support the following:
- We aim to enable the capability to search across the entire Backstage
ecosystem by decoupling search from content management.
ecosystem including, but not limited to, entities in the software catalog.
Searchable content won't be required to relate directly to the software
catalog, but by convention, we may encourage loose relationships using
well-known field names or attributes.
- We aim to enable the capability to deploy Backstage using any search engine,
by providing an integration and translation layer between the core search
plugin and search engine specific logic that can be extended for different
@@ -29,11 +32,17 @@ At a base-level, we want to support the following:
More advanced use-cases we hope to support with this architecture include:
- It should be easy for any plugin to expose new content to search. (e.g. entity
metadata, documentation from TechDocs)
- It should be easy for any plugin to append relevant metadata to existing
- It should be possible for any plugin to expose new content to search. (e.g.
entity metadata, documentation from TechDocs)
- It should be possible for any plugin to append relevant metadata to existing
content in search. (e.g. location (path) for TechDocs page)
- It should be easy to refine search queries (e.g. ranking, scoring, etc.)
- It should be easy to customize the search UI
- It should be easy to add search functionality to any Backstage plugin or
- It should be possible to refine search queries (e.g. ranking, scoring, etc.)
- It should be possible to customize the search UI
- It should be possible to add search functionality to any Backstage plugin or
deployment
Architecture non-goals:
- At this time, we do not intend to directly support event-driven or incremental
index management. Instead, we'll be focused on scheduled, bulk index
management.
+77 -6
View File
@@ -27,11 +27,15 @@ REPOSITORY_URL='https://github.com/org/repo'
git clone $REPOSITORY_URL
cd repo
# Install @techdocs/cli, mkdocs and mkdocs plugins
npm install -g @techdocs/cli
pip install mkdocs-techdocs-core==0.*
# Generate
npx @techdocs/cli generate
techdocs-cli generate --no-docker
# Publish
npx @techdocs/cli publish --publisher-type awsS3 --storage-name <bucket/container> --entity <Namespace/Kind/Name>
techdocs-cli publish --publisher-type awsS3 --storage-name <bucket/container> --entity <Namespace/Kind/Name>
```
That's it!
@@ -40,14 +44,16 @@ Take a look at
[`techdocs-cli` README](https://github.com/backstage/techdocs-cli) for the
complete command reference, details, and options.
## 1. Setup a workflow
## Steps
### 1. Setup a workflow
The TechDocs workflow should trigger on CI when any changes are made in the
repository containing the documentation files. You can be specific and configure
the workflow to be triggered only when files inside the `docs/` directory or
`mkdocs.yml` are changed.
## 2. Prepare step
### 2. Prepare step
The first step on the CI is to clone your documentation source repository in a
working directory. This is almost always the first step in most CI workflows.
@@ -62,7 +68,7 @@ step.
Eventually we are trying to do a `git clone <https://path/to/docs-repository/>`.
## 3. Generate step
### 3. Generate step
Install [`npx`](https://www.npmjs.com/package/npx) to use it for running
`techdocs-cli`. Or you can install using `npm install -g @techdocs/cli`.
@@ -78,7 +84,7 @@ npx @techdocs/cli generate --no-docker --source-dir PATH_TO_REPO --output-dir ./
`PATH_TO_REPO` should be the location in the file path where the prepare step
above clones the repository.
## 4. Publish step
### 4. Publish step
Depending on your cloud storage provider (AWS, Google Cloud, or Azure), set the
necessary authentication environment variables.
@@ -96,3 +102,68 @@ npx @techdocs/cli publish --publisher-type <awsS3|googleGcs> --storage-name <buc
The updated TechDocs site built in this workflow is now ready to be served by
the TechDocs plugin in your Backstage app.
## Example: GitHub Actions CI and AWS S3
Here is an example workflow using GitHub Actions CI and AWS S3 storage. You can
use any CI and any other
[TechDocs supported cloud storage providers](README.md#platforms-supported).
Add a `.github/workflows/techdocs.yml` file in your
[Software Template(s)](../software-templates/index.md) like this -
```yaml
name: Publish TechDocs Site
on:
push:
branches: [main]
# You can even set it to run only when TechDocs related files are updated.
# paths:
# - "docs/**"
# - "mkdocs.yml"
jobs:
publish-techdocs-site:
runs-on: ubuntu-latest
# The following secrets are required in your CI environment for publishing files to AWS S3.
# e.g. You can use GitHub Organization secrets to set them for all existing and new repositories.
env:
TECHDOCS_S3_BUCKET_NAME: ${{ secrets.TECHDOCS_S3_BUCKET_NAME }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ secrets.AWS_REGION }}
ENTITY_NAMESPACE: 'default'
ENTITY_KIND: 'Component'
ENTITY_NAME: 'my-doc-entity'
# In a Software template, Scaffolder will replace {{cookiecutter.component_id | jsonify}}
# with the correct entity name. This is same as metadata.name in the entity's catalog-info.yaml
# ENTITY_NAME: '{{ cookiecutter.component_id | jsonify }}'
steps:
- name: Checkout code
uses: actions/checkout@v2
- uses: actions/setup-node@v2
- uses: actions/setup-python@v2
- name: Install techdocs-cli
run: sudo npm install -g @techdocs/cli
- name: Install mkdocs and mkdocs plugins
run: python -m pip install mkdocs-techdocs-core==0.*
- name: Generate docs site
run: techdocs-cli generate --no-docker --verbose
- name: Publish docs site
run:
techdocs-cli publish --publisher-type awsS3 --storage-name
$TECHDOCS_S3_BUCKET_NAME --entity
$ENTITY_NAMESPACE/$ENTITY_KIND/$ENTITY_NAME
```
When the new repository is scaffolded or new documentation updates are
committed, the GitHub Action workflow will publish the TechDocs site, which can
be viewed in your Backstage app.
+8 -6
View File
@@ -157,15 +157,17 @@ techdocs:
builder: 'local'
```
Set `techdocs.builder` to `'local'` if you want your TechDocs Backend to be
responsible for generating documentation sites. If set to `'external'`,
Backstage will assume that the sites are being generated on each entity's CI/CD
pipeline, and are being stored in a storage somewhere.
Note that we recommend generating docs on CI/CD instead. Read more in the
"Basic" and "Recommended" sections of the
[TechDocs Architecture](architecture.md). But if you want to get started quickly
set `techdocs.builder` to `'local'` so that TechDocs Backend is responsible for
generating documentation sites. If set to `'external'`, Backstage will assume
that the sites are being generated on each entity's CI/CD pipeline, and are
being stored in a storage somewhere.
When `techdocs.builder` is set to `'external'`, TechDocs becomes more or less a
read-only experience where it serves static files from a storage containing all
the generated documentation. Read more in the "Basic" and "Recommended" sections
of the [TechDocs Architecture](architecture.md).
the generated documentation.
### Choosing storage (publisher)
+7 -5
View File
@@ -99,13 +99,15 @@ Are you missing a plugin for your favorite tool? Please
[suggest a new one](https://github.com/backstage/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME).
Chances are that someone will jump in and help build it.
### Community Initiatives 🧑‍🤝‍🧑 (Coming soon)
### Community Initiatives 🧑‍🤝‍🧑
- **Backstage Monthly Meetup** - A space for the community to come together to
share and learn about the latest happenings in Backstage.
- [**Backstage Community Sessions**](https://github.com/backstage/community#meetups) -
A monthly meetup for the community to come together to share and learn about
the latest happenings in Backstage.
- **Backstage Hackathons** - Open to everyone in our Backstage community, a
celebration of you, the project and building awesome things together
- **Backstage Hackathons** - (Coming soon) Open to everyone in our Backstage
community, a celebration of you, the project and building awesome things
together
### Completed milestones ✅
-8
View File
@@ -384,11 +384,3 @@ A frontend plugin that provides a page where the user can tweak various
settings.
Stability: `1`
### `welcome` [GitHub](https://github.com/backstage/backstage/tree/master/plugins/welcome/)
A plugin that can be used to welcome the user to Backstage.
Stability: `0`. This used to be the start page for the example app, but has been
replaced by the catalog plugin. It is still viewable at `/welcome` but may be
removed.
+1 -1
View File
@@ -14,7 +14,7 @@ Here's a code sample:
import { createPlugin } from '@backstage/core';
export default createPlugin({
id: 'welcome',
id: 'plugin-name',
register({ featureFlags }) {
featureFlags.register('enable-example-feature');
},
+5 -5
View File
@@ -1,7 +1,7 @@
# AlertApi
The AlertApi type is defined at
[packages/core-api/src/apis/definitions/AlertApi.ts:29](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AlertApi.ts#L29).
[packages/core-api/src/apis/definitions/AlertApi.ts:29](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AlertApi.ts#L29).
The following Utility API implements this type: [alertApiRef](./README.md#alert)
@@ -38,7 +38,7 @@ export type AlertMessage = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AlertApi.ts#L19).
[packages/core-api/src/apis/definitions/AlertApi.ts:19](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AlertApi.ts#L19).
Referenced by: [post](#post), [alert\$](#alert).
@@ -67,7 +67,7 @@ export type Observable&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53).
[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53).
Referenced by: [alert\$](#alert).
@@ -86,7 +86,7 @@ export type Observer&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24).
[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
@@ -109,6 +109,6 @@ export type Subscription = {
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33).
[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
+10 -10
View File
@@ -1,7 +1,7 @@
# AppThemeApi
The AppThemeApi type is defined at
[packages/core-api/src/apis/definitions/AppThemeApi.ts:56](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AppThemeApi.ts#L56).
[packages/core-api/src/apis/definitions/AppThemeApi.ts:56](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AppThemeApi.ts#L56).
The following Utility API implements this type:
[appThemeApiRef](./README.md#apptheme)
@@ -81,7 +81,7 @@ export type AppTheme = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/AppThemeApi.ts:25](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AppThemeApi.ts#L25).
[packages/core-api/src/apis/definitions/AppThemeApi.ts:25](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AppThemeApi.ts#L25).
Referenced by: [getInstalledThemes](#getinstalledthemes).
@@ -92,7 +92,7 @@ export type BackstagePalette = Palette &amp; <a href="#paletteadditions">Palette
</pre>
Defined at
[packages/theme/src/types.ts:74](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/theme/src/types.ts#L74).
[packages/theme/src/types.ts:74](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L74).
Referenced by: [BackstageTheme](#backstagetheme).
@@ -107,7 +107,7 @@ export interface BackstageTheme extends Theme {
</pre>
Defined at
[packages/theme/src/types.ts:81](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/theme/src/types.ts#L81).
[packages/theme/src/types.ts:81](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L81).
Referenced by: [AppTheme](#apptheme).
@@ -136,7 +136,7 @@ export type Observable&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53).
[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53).
Referenced by: [activeThemeId\$](#activethemeid).
@@ -155,7 +155,7 @@ export type Observer&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24).
[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
@@ -170,7 +170,7 @@ export type PageTheme = {
</pre>
Defined at
[packages/theme/src/types.ts:103](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/theme/src/types.ts#L103).
[packages/theme/src/types.ts:103](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L103).
Referenced by: [BackstageTheme](#backstagetheme).
@@ -183,7 +183,7 @@ export type PageThemeSelector = {
</pre>
Defined at
[packages/theme/src/types.ts:77](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/theme/src/types.ts#L77).
[packages/theme/src/types.ts:77](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L77).
Referenced by: [BackstageTheme](#backstagetheme).
@@ -243,7 +243,7 @@ type PaletteAdditions = {
</pre>
Defined at
[packages/theme/src/types.ts:23](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/theme/src/types.ts#L23).
[packages/theme/src/types.ts:23](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/theme/src/types.ts#L23).
Referenced by: [BackstagePalette](#backstagepalette).
@@ -266,6 +266,6 @@ export type Subscription = {
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33).
[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
@@ -1,7 +1,7 @@
# BackstageIdentityApi
The BackstageIdentityApi type is defined at
[packages/core-api/src/apis/definitions/auth.ts:134](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L134).
[packages/core-api/src/apis/definitions/auth.ts:134](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L134).
The following Utility APIs implement this type:
@@ -17,8 +17,12 @@ The following Utility APIs implement this type:
- [oauth2ApiRef](./README.md#oauth2)
- [oidcAuthApiRef](./README.md#oidcauth)
- [oktaAuthApiRef](./README.md#oktaauth)
- [oneloginAuthApiRef](./README.md#oneloginauth)
- [samlAuthApiRef](./README.md#samlauth)
## Members
@@ -70,7 +74,7 @@ export type AuthRequestOptions = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L40).
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L40).
Referenced by: [getBackstageIdentity](#getbackstageidentity).
@@ -79,7 +83,7 @@ Referenced by: [getBackstageIdentity](#getbackstageidentity).
<pre>
export type BackstageIdentity = {
/**
* The Backstage user ID.
* The backstage user ID.
*/
id: string;
@@ -91,6 +95,6 @@ export type BackstageIdentity = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:147](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L147).
[packages/core-api/src/apis/definitions/auth.ts:147](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L147).
Referenced by: [getBackstageIdentity](#getbackstageidentity).
+5 -5
View File
@@ -1,7 +1,7 @@
# Config
The Config type is defined at
[packages/config/src/types.ts:32](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/config/src/types.ts#L32).
[packages/config/src/types.ts:32](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L32).
The following Utility API implements this type:
[configApiRef](./README.md#config)
@@ -140,7 +140,7 @@ export type Config = {
</pre>
Defined at
[packages/config/src/types.ts:32](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/config/src/types.ts#L32).
[packages/config/src/types.ts:32](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L32).
Referenced by: [getConfig](#getconfig), [getOptionalConfig](#getoptionalconfig),
[getConfigArray](#getconfigarray),
@@ -153,7 +153,7 @@ export type JsonArray = <a href="#jsonvalue">JsonValue</a>[]
</pre>
Defined at
[packages/config/src/types.ts:18](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/config/src/types.ts#L18).
[packages/config/src/types.ts:18](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L18).
Referenced by: [JsonValue](#jsonvalue).
@@ -164,7 +164,7 @@ export type JsonObject = { [key in string]?: <a href="#jsonvalue">JsonValue</a>
</pre>
Defined at
[packages/config/src/types.ts:17](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/config/src/types.ts#L17).
[packages/config/src/types.ts:17](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L17).
Referenced by: [JsonValue](#jsonvalue).
@@ -181,7 +181,7 @@ export type JsonValue =
</pre>
Defined at
[packages/config/src/types.ts:19](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/config/src/types.ts#L19).
[packages/config/src/types.ts:19](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/config/src/types.ts#L19).
Referenced by: [get](#get), [getOptional](#getoptional),
[JsonObject](#jsonobject), [JsonArray](#jsonarray), [Config](#config).
+2 -2
View File
@@ -1,7 +1,7 @@
# DiscoveryApi
The DiscoveryApi type is defined at
[packages/core-api/src/apis/definitions/DiscoveryApi.ts:30](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L30).
[packages/core-api/src/apis/definitions/DiscoveryApi.ts:30](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L30).
The following Utility API implements this type:
[discoveryApiRef](./README.md#discovery)
@@ -12,7 +12,7 @@ The following Utility API implements this type:
Returns the HTTP base backend URL for a given plugin, without a trailing slash.
This method must always be called just before making a request. as opposed to
This method must always be called just before making a request, as opposed to
fetching the URL when constructing an API client. That is to ensure that more
flexible routing patterns can be supported.
+6 -6
View File
@@ -1,7 +1,7 @@
# ErrorApi
The ErrorApi type is defined at
[packages/core-api/src/apis/definitions/ErrorApi.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/ErrorApi.ts#L53).
[packages/core-api/src/apis/definitions/ErrorApi.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ErrorApi.ts#L53).
The following Utility API implements this type: [errorApiRef](./README.md#error)
@@ -41,7 +41,7 @@ type Error = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/ErrorApi.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/ErrorApi.ts#L24).
[packages/core-api/src/apis/definitions/ErrorApi.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ErrorApi.ts#L24).
Referenced by: [post](#post), [error\$](#error).
@@ -58,7 +58,7 @@ export type ErrorContext = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/ErrorApi.ts#L33).
[packages/core-api/src/apis/definitions/ErrorApi.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ErrorApi.ts#L33).
Referenced by: [post](#post), [error\$](#error).
@@ -87,7 +87,7 @@ export type Observable&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53).
[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53).
Referenced by: [error\$](#error).
@@ -106,7 +106,7 @@ export type Observer&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24).
[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
@@ -129,6 +129,6 @@ export type Subscription = {
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33).
[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
@@ -1,7 +1,7 @@
# FeatureFlagsApi
The FeatureFlagsApi type is defined at
[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:60](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L60).
[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:60](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L60).
The following Utility API implements this type:
[featureFlagsApiRef](./README.md#featureflags)
@@ -68,7 +68,7 @@ export type FeatureFlag = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:31](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L31).
[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:31](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L31).
Referenced by: [registerFlag](#registerflag),
[getRegisteredFlags](#getregisteredflags).
@@ -83,7 +83,7 @@ export enum FeatureFlagState {
</pre>
Defined at
[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:36](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L36).
[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:36](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L36).
Referenced by: [FeatureFlagsSaveOptions](#featureflagssaveoptions).
@@ -108,6 +108,6 @@ export type FeatureFlagsSaveOptions = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:44](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L44).
[packages/core-api/src/apis/definitions/FeatureFlagsApi.ts:44](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L44).
Referenced by: [save](#save).
+2 -2
View File
@@ -1,7 +1,7 @@
# IdentityApi
The IdentityApi type is defined at
[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/IdentityApi.ts#L22).
[packages/core-api/src/apis/definitions/IdentityApi.ts:22](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/IdentityApi.ts#L22).
The following Utility API implements this type:
[identityApiRef](./README.md#identity)
@@ -76,6 +76,6 @@ export type ProfileInfo = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:162](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L162).
[packages/core-api/src/apis/definitions/auth.ts:162](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L162).
Referenced by: [getProfile](#getprofile).
+7 -3
View File
@@ -1,7 +1,7 @@
# OAuthApi
The OAuthApi type is defined at
[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L67).
[packages/core-api/src/apis/definitions/auth.ts:67](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L67).
The following Utility APIs implement this type:
@@ -15,8 +15,12 @@ The following Utility APIs implement this type:
- [oauth2ApiRef](./README.md#oauth2)
- [oidcAuthApiRef](./README.md#oidcauth)
- [oktaAuthApiRef](./README.md#oktaauth)
- [oneloginAuthApiRef](./README.md#oneloginauth)
## Members
### getAccessToken()
@@ -82,7 +86,7 @@ export type AuthRequestOptions = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L40).
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L40).
Referenced by: [getAccessToken](#getaccesstoken).
@@ -108,6 +112,6 @@ export type OAuthScope = string | string[]
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L38).
[packages/core-api/src/apis/definitions/auth.ts:38](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L38).
Referenced by: [getAccessToken](#getaccesstoken).
@@ -1,7 +1,7 @@
# OAuthRequestApi
The OAuthRequestApi type is defined at
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99).
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:99](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L99).
The following Utility API implements this type:
[oauthRequestApiRef](./README.md#oauthrequest)
@@ -73,7 +73,7 @@ export type AuthProvider = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L27).
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:27](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L27).
Referenced by: [AuthRequesterOptions](#authrequesteroptions),
[PendingAuthRequest](#pendingauthrequest).
@@ -97,7 +97,7 @@ export type AuthRequester&lt;AuthResponse&gt; = (
</pre>
Defined at
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:66](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L66).
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:66](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L66).
Referenced by: [createAuthRequester](#createauthrequester).
@@ -122,7 +122,7 @@ export type AuthRequesterOptions&lt;AuthResponse&gt; = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L43).
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:43](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L43).
Referenced by: [createAuthRequester](#createauthrequester).
@@ -151,7 +151,7 @@ export type Observable&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53).
[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53).
Referenced by: [authRequest\$](#authrequest).
@@ -170,7 +170,7 @@ export type Observer&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24).
[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
@@ -205,7 +205,7 @@ export type PendingAuthRequest = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77).
[packages/core-api/src/apis/definitions/OAuthRequestApi.ts:77](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L77).
Referenced by: [authRequest\$](#authrequest).
@@ -228,6 +228,6 @@ export type Subscription = {
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33).
[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
@@ -1,7 +1,7 @@
# OpenIdConnectApi
The OpenIdConnectApi type is defined at
[packages/core-api/src/apis/definitions/auth.ts:99](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L99).
[packages/core-api/src/apis/definitions/auth.ts:99](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L99).
The following Utility APIs implement this type:
@@ -13,8 +13,12 @@ The following Utility APIs implement this type:
- [oauth2ApiRef](./README.md#oauth2)
- [oidcAuthApiRef](./README.md#oidcauth)
- [oktaAuthApiRef](./README.md#oktaauth)
- [oneloginAuthApiRef](./README.md#oneloginauth)
## Members
### getIdToken()
@@ -66,6 +70,6 @@ export type AuthRequestOptions = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L40).
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L40).
Referenced by: [getIdToken](#getidtoken).
@@ -1,7 +1,7 @@
# ProfileInfoApi
The ProfileInfoApi type is defined at
[packages/core-api/src/apis/definitions/auth.ts:117](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L117).
[packages/core-api/src/apis/definitions/auth.ts:117](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L117).
The following Utility APIs implement this type:
@@ -17,8 +17,12 @@ The following Utility APIs implement this type:
- [oauth2ApiRef](./README.md#oauth2)
- [oidcAuthApiRef](./README.md#oidcauth)
- [oktaAuthApiRef](./README.md#oktaauth)
- [oneloginAuthApiRef](./README.md#oneloginauth)
- [samlAuthApiRef](./README.md#samlauth)
## Members
@@ -67,7 +71,7 @@ export type AuthRequestOptions = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L40).
[packages/core-api/src/apis/definitions/auth.ts:40](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L40).
Referenced by: [getProfile](#getprofile).
@@ -95,6 +99,6 @@ export type ProfileInfo = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:162](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L162).
[packages/core-api/src/apis/definitions/auth.ts:162](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L162).
Referenced by: [getProfile](#getprofile).
+41 -17
View File
@@ -12,7 +12,7 @@ Used to report alerts and forward them to the app
Implemented type: [AlertApi](./AlertApi.md)
ApiRef:
[alertApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AlertApi.ts#L41)
[alertApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AlertApi.ts#L41)
### appTheme
@@ -21,7 +21,7 @@ API Used to configure the app theme, and enumerate options
Implemented type: [AppThemeApi](./AppThemeApi.md)
ApiRef:
[appThemeApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/AppThemeApi.ts#L80)
[appThemeApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/AppThemeApi.ts#L80)
### auth0Auth
@@ -32,7 +32,7 @@ Implemented types: [OpenIdConnectApi](./OpenIdConnectApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md)
ApiRef:
[auth0AuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L275)
[auth0AuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L275)
### config
@@ -41,7 +41,7 @@ Used to access runtime configuration
Implemented type: [Config](./Config.md)
ApiRef:
[configApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/ConfigApi.ts#L22)
[configApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ConfigApi.ts#L25)
### discovery
@@ -50,7 +50,7 @@ Provides service discovery of backend plugins
Implemented type: [DiscoveryApi](./DiscoveryApi.md)
ApiRef:
[discoveryApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L44)
[discoveryApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/DiscoveryApi.ts#L44)
### error
@@ -59,7 +59,7 @@ Used to report errors and forward them to the app
Implemented type: [ErrorApi](./ErrorApi.md)
ApiRef:
[errorApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/ErrorApi.ts#L65)
[errorApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/ErrorApi.ts#L65)
### featureFlags
@@ -68,7 +68,7 @@ Used to toggle functionality in features across Backstage
Implemented type: [FeatureFlagsApi](./FeatureFlagsApi.md)
ApiRef:
[featureFlagsApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L83)
[featureFlagsApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts#L83)
### githubAuth
@@ -79,7 +79,7 @@ Implemented types: [OAuthApi](./OAuthApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md)
ApiRef:
[githubAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L232)
[githubAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L232)
### gitlabAuth
@@ -90,7 +90,7 @@ Implemented types: [OAuthApi](./OAuthApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md)
ApiRef:
[gitlabAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L262)
[gitlabAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L262)
### googleAuth
@@ -102,7 +102,7 @@ Implemented types: [OAuthApi](./OAuthApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md)
ApiRef:
[googleAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L215)
[googleAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L215)
### identity
@@ -111,7 +111,7 @@ Provides access to the identity of the signed in user
Implemented type: [IdentityApi](./IdentityApi.md)
ApiRef:
[identityApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/IdentityApi.ts#L54)
[identityApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/IdentityApi.ts#L53)
### microsoftAuth
@@ -123,7 +123,7 @@ Implemented types: [OAuthApi](./OAuthApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md)
ApiRef:
[microsoftAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L289)
[microsoftAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L289)
### oauth2
@@ -135,7 +135,7 @@ Implemented types: [OAuthApi](./OAuthApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md)
ApiRef:
[oauth2ApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L303)
[oauth2ApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L303)
### oauthRequest
@@ -144,7 +144,19 @@ An API for implementing unified OAuth flows in Backstage
Implemented type: [OAuthRequestApi](./OAuthRequestApi.md)
ApiRef:
[oauthRequestApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130)
[oauthRequestApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/OAuthRequestApi.ts#L130)
### oidcAuth
Example of how to use oidc custom provider
Implemented types: [OAuthApi](./OAuthApi.md),
[OpenIdConnectApi](./OpenIdConnectApi.md),
[ProfileInfoApi](./ProfileInfoApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md)
ApiRef:
[oidcAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L317)
### oktaAuth
@@ -156,7 +168,19 @@ Implemented types: [OAuthApi](./OAuthApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md)
ApiRef:
[oktaAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L245)
[oktaAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L245)
### oneloginAuth
Provides authentication towards OneLogin APIs and identities
Implemented types: [OAuthApi](./OAuthApi.md),
[OpenIdConnectApi](./OpenIdConnectApi.md),
[ProfileInfoApi](./ProfileInfoApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md)
ApiRef:
[oneloginAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L338)
### samlAuth
@@ -166,7 +190,7 @@ Implemented types: [ProfileInfoApi](./ProfileInfoApi.md),
[BackstageIdentityApi](./BackstageIdentityApi.md), [SessionApi](./SessionApi.md)
ApiRef:
[samlAuthApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L317)
[samlAuthApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L331)
### storage
@@ -175,4 +199,4 @@ Provides the ability to store data which is unique to the user
Implemented type: [StorageApi](./StorageApi.md)
ApiRef:
[storageApiRef](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/StorageApi.ts#L68)
[storageApiRef](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/StorageApi.ts#L68)
+9 -5
View File
@@ -1,7 +1,7 @@
# SessionApi
The SessionApi type is defined at
[packages/core-api/src/apis/definitions/auth.ts:190](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L190).
[packages/core-api/src/apis/definitions/auth.ts:190](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L190).
The following Utility APIs implement this type:
@@ -17,8 +17,12 @@ The following Utility APIs implement this type:
- [oauth2ApiRef](./README.md#oauth2)
- [oidcAuthApiRef](./README.md#oidcauth)
- [oktaAuthApiRef](./README.md#oktaauth)
- [oneloginAuthApiRef](./README.md#oneloginauth)
- [samlAuthApiRef](./README.md#samlauth)
## Members
@@ -77,7 +81,7 @@ export type Observable&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53).
[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53).
Referenced by: [sessionState\$](#sessionstate).
@@ -96,7 +100,7 @@ export type Observer&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24).
[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
@@ -112,7 +116,7 @@ export enum SessionState {
</pre>
Defined at
[packages/core-api/src/apis/definitions/auth.ts:182](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/auth.ts#L182).
[packages/core-api/src/apis/definitions/auth.ts:182](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/auth.ts#L182).
Referenced by: [sessionState\$](#sessionstate).
@@ -135,6 +139,6 @@ export type Subscription = {
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33).
[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
+6 -6
View File
@@ -1,7 +1,7 @@
# StorageApi
The StorageApi type is defined at
[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/StorageApi.ts#L31).
[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/StorageApi.ts#L31).
The following Utility API implements this type:
[storageApiRef](./README.md#storage)
@@ -79,7 +79,7 @@ export type Observable&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L53).
[packages/core-api/src/types.ts:53](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L53).
Referenced by: [observe\$](#observe), [StorageApi](#storageapi).
@@ -98,7 +98,7 @@ export type Observer&lt;T&gt; = {
</pre>
Defined at
[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L24).
[packages/core-api/src/types.ts:24](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L24).
Referenced by: [Observable](#observable).
@@ -144,7 +144,7 @@ export interface StorageApi {
</pre>
Defined at
[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/StorageApi.ts#L31).
[packages/core-api/src/apis/definitions/StorageApi.ts:31](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/StorageApi.ts#L31).
Referenced by: [forBucket](#forbucket).
@@ -158,7 +158,7 @@ export type StorageValueChange&lt;T = any&gt; = {
</pre>
Defined at
[packages/core-api/src/apis/definitions/StorageApi.ts:21](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/apis/definitions/StorageApi.ts#L21).
[packages/core-api/src/apis/definitions/StorageApi.ts:21](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/apis/definitions/StorageApi.ts#L21).
Referenced by: [observe\$](#observe), [StorageApi](#storageapi).
@@ -181,6 +181,6 @@ export type Subscription = {
</pre>
Defined at
[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/ca535f2f66c3a4980c80f4b1a049dfd07569010e/packages/core-api/src/types.ts#L33).
[packages/core-api/src/types.ts:33](https://github.com/backstage/backstage/blob/a4dbd8353cfa4d4d4334473e2c33afcda64e130d/packages/core-api/src/types.ts#L33).
Referenced by: [Observable](#observable).
+4
View File
@@ -1115,6 +1115,10 @@ code {
font-size: 115%;
}
.post pre {
color: #e4e4e4;
}
.Block__GIF {
width: 190px;
height: auto;
+86
View File
@@ -1,5 +1,91 @@
# example-app
## 0.2.15
### Patch Changes
- 07bafa248: Add configurable `scope` for oauth2 auth provider.
Some OAuth2 providers require certain scopes to facilitate a user sign-in using the Authorization Code flow.
This change adds the optional `scope` key to auth.providers.oauth2. An example is:
```yaml
auth:
providers:
oauth2:
development:
clientId:
$env: DEV_OAUTH2_CLIENT_ID
clientSecret:
$env: DEV_OAUTH2_CLIENT_SECRET
authorizationUrl:
$env: DEV_OAUTH2_AUTH_URL
tokenUrl:
$env: DEV_OAUTH2_TOKEN_URL
scope: saml-login-selector openid profile email
```
This tells the OAuth 2.0 AS to perform a SAML login and return OIDC information include the `profile`
and `email` claims as part of the ID Token.
- Updated dependencies [753bb4c40]
- Updated dependencies [6ed2b47d6]
- Updated dependencies [b33fa4cf4]
- Updated dependencies [d36660721]
- Updated dependencies [6b26c9f41]
- Updated dependencies [b3f0c3811]
- Updated dependencies [302795d10]
- Updated dependencies [9ec66c345]
- Updated dependencies [53d3e2d62]
- Updated dependencies [ca559171b]
- Updated dependencies [53348f0af]
- Updated dependencies [f5e564cd6]
- Updated dependencies [68dd79d83]
- Updated dependencies [29a138636]
- Updated dependencies [14aef4b94]
- Updated dependencies [41af18227]
- Updated dependencies [1df75733e]
- Updated dependencies [02d6803e8]
- Updated dependencies [b288a291e]
- Updated dependencies [025c0c7bf]
- Updated dependencies [e5da858d7]
- Updated dependencies [9230d07e7]
- Updated dependencies [f5f45744e]
- Updated dependencies [0fe8ff5be]
- Updated dependencies [c5ab91ce3]
- Updated dependencies [64b9efac2]
- Updated dependencies [19d354c78]
- Updated dependencies [7716d1d70]
- Updated dependencies [8f3443427]
- Updated dependencies [b51ee6ece]
- Updated dependencies [accdfeb30]
- Updated dependencies [804502a5c]
- @backstage/plugin-catalog-import@0.4.0
- @backstage/plugin-catalog@0.3.1
- @backstage/plugin-kubernetes@0.3.9
- @backstage/plugin-rollbar@0.3.0
- @backstage/plugin-scaffolder@0.5.0
- @backstage/plugin-cost-insights@0.8.1
- @backstage/plugin-circleci@0.2.8
- @backstage/plugin-search@0.3.0
- @backstage/plugin-cloudbuild@0.2.9
- @backstage/plugin-register-component@0.2.9
- @backstage/plugin-sentry@0.3.5
- @backstage/plugin-jenkins@0.3.9
- @backstage/plugin-api-docs@0.4.5
- @backstage/plugin-lighthouse@0.2.10
- @backstage/plugin-techdocs@0.5.6
- @backstage/plugin-pagerduty@0.2.8
- @backstage/plugin-org@0.3.6
- @backstage/plugin-github-actions@0.3.2
- @backstage/plugin-explore@0.2.5
- @backstage/plugin-newrelic@0.2.5
- @backstage/plugin-catalog-react@0.0.3
- @backstage/plugin-kafka@0.2.2
- @backstage/core@0.6.1
- @backstage/plugin-gitops-profiles@0.2.5
- @backstage/plugin-tech-radar@0.3.5
## 0.2.14
### Patch Changes
+27 -28
View File
@@ -1,40 +1,39 @@
{
"name": "example-app",
"version": "0.2.14",
"version": "0.2.15",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.7.1",
"@backstage/cli": "^0.6.0",
"@backstage/core": "^0.6.0",
"@backstage/plugin-api-docs": "^0.4.4",
"@backstage/plugin-catalog": "^0.3.0",
"@backstage/plugin-catalog-react": "^0.0.2",
"@backstage/plugin-catalog-import": "^0.3.7",
"@backstage/plugin-circleci": "^0.2.7",
"@backstage/plugin-cloudbuild": "^0.2.8",
"@backstage/plugin-cost-insights": "^0.8.0",
"@backstage/plugin-explore": "^0.2.4",
"@backstage/core": "^0.6.1",
"@backstage/plugin-api-docs": "^0.4.5",
"@backstage/plugin-catalog": "^0.3.1",
"@backstage/plugin-catalog-react": "^0.0.3",
"@backstage/plugin-catalog-import": "^0.4.0",
"@backstage/plugin-circleci": "^0.2.8",
"@backstage/plugin-cloudbuild": "^0.2.9",
"@backstage/plugin-cost-insights": "^0.8.1",
"@backstage/plugin-explore": "^0.2.5",
"@backstage/plugin-gcp-projects": "^0.2.4",
"@backstage/plugin-github-actions": "^0.3.1",
"@backstage/plugin-gitops-profiles": "^0.2.4",
"@backstage/plugin-github-actions": "^0.3.2",
"@backstage/plugin-gitops-profiles": "^0.2.5",
"@backstage/plugin-graphiql": "^0.2.7",
"@backstage/plugin-org": "^0.3.5",
"@backstage/plugin-jenkins": "^0.3.8",
"@backstage/plugin-kafka": "^0.2.1",
"@backstage/plugin-kubernetes": "^0.3.8",
"@backstage/plugin-lighthouse": "^0.2.9",
"@backstage/plugin-newrelic": "^0.2.4",
"@backstage/plugin-pagerduty": "0.2.7",
"@backstage/plugin-register-component": "^0.2.8",
"@backstage/plugin-rollbar": "^0.2.9",
"@backstage/plugin-scaffolder": "^0.4.2",
"@backstage/plugin-sentry": "^0.3.4",
"@backstage/plugin-search": "^0.2.7",
"@backstage/plugin-tech-radar": "^0.3.4",
"@backstage/plugin-techdocs": "^0.5.5",
"@backstage/plugin-org": "^0.3.6",
"@backstage/plugin-jenkins": "^0.3.9",
"@backstage/plugin-kafka": "^0.2.2",
"@backstage/plugin-kubernetes": "^0.3.9",
"@backstage/plugin-lighthouse": "^0.2.10",
"@backstage/plugin-newrelic": "^0.2.5",
"@backstage/plugin-pagerduty": "0.2.8",
"@backstage/plugin-register-component": "^0.2.9",
"@backstage/plugin-rollbar": "^0.3.0",
"@backstage/plugin-scaffolder": "^0.5.0",
"@backstage/plugin-sentry": "^0.3.5",
"@backstage/plugin-search": "^0.3.0",
"@backstage/plugin-tech-radar": "^0.3.5",
"@backstage/plugin-techdocs": "^0.5.6",
"@backstage/plugin-user-settings": "^0.2.5",
"@backstage/plugin-welcome": "^0.2.5",
"@backstage/theme": "^0.2.3",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
@@ -54,7 +53,7 @@
"zen-observable": "^0.8.15"
},
"devDependencies": {
"@backstage/test-utils": "^0.1.6",
"@backstage/test-utils": "^0.1.7",
"@testing-library/cypress": "^7.0.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^10.4.1",
+2 -5
View File
@@ -23,7 +23,7 @@ import {
SignInPage,
} from '@backstage/core';
import { Router as CatalogRouter } from '@backstage/plugin-catalog';
import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import';
import { CatalogImportPage } from '@backstage/plugin-catalog-import';
import { ExplorePage } from '@backstage/plugin-explore';
import { Router as GraphiQLRouter } from '@backstage/plugin-graphiql';
import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse';
@@ -69,10 +69,7 @@ const catalogRouteRef = createRouteRef({
const routes = (
<FlatRoutes>
<Navigate key="/" to="/catalog" />
<Route
path="/catalog-import"
element={<ImportComponentRouter catalogRouteRef={catalogRouteRef} />}
/>
<Route path="/catalog-import" element={<CatalogImportPage />} />
<Route
path={`${catalogRouteRef.path}`}
element={<CatalogRouter EntityPage={EntityPage} />}
@@ -28,7 +28,11 @@ import {
ProvidedApisCard,
ProvidingComponentsCard,
} from '@backstage/plugin-api-docs';
import { AboutCard, EntityPageLayout } from '@backstage/plugin-catalog';
import {
AboutCard,
EntityLinksCard,
EntityPageLayout,
} from '@backstage/plugin-catalog';
import { useEntity } from '@backstage/plugin-catalog-react';
import {
isPluginApplicableToEntity as isCircleCIAvailable,
@@ -177,6 +181,9 @@ const ComponentOverviewContent = ({ entity }: { entity: Entity }) => (
<PagerDutyCard entity={entity} />
</Grid>
)}
<Grid item md={4} sm={6}>
<EntityLinksCard entity={entity} />
</Grid>
<RecentCICDRunsSwitcher entity={entity} />
{isGitHubAvailable(entity) && (
<>
+7
View File
@@ -22,6 +22,7 @@ import {
samlAuthApiRef,
microsoftAuthApiRef,
oneloginAuthApiRef,
oauth2ApiRef,
oidcAuthApiRef,
} from '@backstage/core';
@@ -32,6 +33,12 @@ export const providers = [
message: 'Sign In using OpenId Connect',
apiRef: oidcAuthApiRef,
},
{
id: 'oauth2-auth-provider',
title: 'OAuth 2.0',
message: 'Sign In using OAuth 2.0',
apiRef: oauth2ApiRef,
},
{
id: 'google-auth-provider',
title: 'Google',
-1
View File
@@ -13,7 +13,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { plugin as WelcomePlugin } from '@backstage/plugin-welcome';
export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse';
export { plugin as CatalogPlugin } from '@backstage/plugin-catalog';
export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder';
+11
View File
@@ -1,5 +1,16 @@
# @backstage/backend-common
## 0.5.3
### Patch Changes
- ffffea8e6: Minor updates to reflect the changes in `@backstage/integration` that made the fields `apiBaseUrl` and `apiUrl` mandatory.
- 82b2c11b6: Set explicit content-type in error handler responses.
- 965e200c6: Slight refactoring in support of a future search implementation in `UrlReader`. Mostly moving code around.
- 5a5163519: Implement `UrlReader.search` which implements glob matching.
- Updated dependencies [ffffea8e6]
- @backstage/integration@0.4.0
## 0.5.2
### Patch Changes
+4 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.5.2",
"version": "0.5.3",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -32,7 +32,7 @@
"@backstage/cli-common": "^0.1.1",
"@backstage/config": "^0.1.2",
"@backstage/config-loader": "^0.5.1",
"@backstage/integration": "^0.3.2",
"@backstage/integration": "^0.4.0",
"@octokit/rest": "^18.0.12",
"@types/cors": "^2.8.6",
"@types/express": "^4.17.6",
@@ -50,6 +50,7 @@
"knex": "^0.21.6",
"lodash": "^4.17.15",
"logform": "^2.1.1",
"minimatch": "^3.0.4",
"minimist": "^1.2.5",
"morgan": "^1.10.0",
"selfsigned": "^1.10.7",
@@ -68,7 +69,7 @@
},
"devDependencies": {
"@backstage/cli": "^0.6.0",
"@backstage/test-utils": "^0.1.5",
"@backstage/test-utils": "^0.1.7",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
"@types/concat-stream": "^1.6.0",
@@ -63,10 +63,10 @@ export function errorHandler(
return (
error: Error,
_request: Request,
response: Response,
res: Response,
next: NextFunction,
) => {
if (response.headersSent) {
if (res.headersSent) {
// If the headers have already been sent, do not send the response again
// as this will throw an error in the backend.
next(error);
@@ -80,7 +80,9 @@ export function errorHandler(
logger.error(error);
}
response.status(status).send(message);
res.status(status);
res.setHeader('content-type', 'text/plain');
res.send(message);
};
}
@@ -14,18 +14,22 @@
* limitations under the License.
*/
import * as os from 'os';
import { ConfigReader } from '@backstage/config';
import {
AzureIntegration,
readAzureIntegrationConfig,
} from '@backstage/integration';
import { msw } from '@backstage/test-utils';
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import path from 'path';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { ConfigReader } from '@backstage/config';
import * as os from 'os';
import path from 'path';
import { NotModifiedError } from '../errors';
import { getVoidLogger } from '../logging';
import { AzureUrlReader } from './AzureUrlReader';
import { msw } from '@backstage/test-utils';
import { ReadTreeResponseFactory } from './tree';
import { NotModifiedError } from '../errors';
const logger = getVoidLogger();
@@ -36,6 +40,16 @@ const treeResponseFactory = ReadTreeResponseFactory.create({
const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
describe('AzureUrlReader', () => {
beforeEach(() => {
mockFs({
[tmpDir]: mockFs.directory(),
});
});
afterEach(() => {
mockFs.restore();
});
const worker = setupServer();
msw.setupDefaultHandlers(worker);
@@ -143,22 +157,18 @@ describe('AzureUrlReader', () => {
});
describe('readTree', () => {
beforeEach(() => {
mockFs({
[tmpDir]: mockFs.directory(),
});
});
afterEach(() => {
mockFs.restore();
});
const repoBuffer = fs.readFileSync(
path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'),
);
const processor = new AzureUrlReader(
{ host: 'dev.azure.com' },
new AzureIntegration(
readAzureIntegrationConfig(
new ConfigReader({
host: 'dev.azure.com',
}),
),
),
{ treeResponseFactory },
);
@@ -257,4 +267,79 @@ describe('AzureUrlReader', () => {
expect(indexMarkdownFile.toString()).toBe('# Test\n');
});
});
describe('search', () => {
const repoBuffer = fs.readFileSync(
path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'),
);
const processor = new AzureUrlReader(
new AzureIntegration(
readAzureIntegrationConfig(
new ConfigReader({
host: 'dev.azure.com',
}),
),
),
{ treeResponseFactory },
);
beforeEach(() => {
worker.use(
rest.get(
'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/zip'),
ctx.body(repoBuffer),
),
),
rest.get(
// https://docs.microsoft.com/en-us/rest/api/azure/devops/git/commits/get%20commits?view=azure-devops-rest-6.0#on-a-branch
'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/commits',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
count: 2,
value: [
{
commitId: '123abc2',
comment: 'second commit',
},
{
commitId: '123abc1',
comment: 'first commit',
},
],
}),
),
),
);
});
it('works for the naive case', async () => {
const result = await processor.search(
'https://dev.azure.com/org-name/project-name/_git/repo-name?path=%2F**%2Findex.*&version=GBmaster',
);
expect(result.etag).toBe('123abc2');
expect(result.files.length).toBe(1);
expect(result.files[0].url).toBe(
'https://dev.azure.com/org-name/project-name/_git/repo-name?path=%2Fdocs%2Findex.md&version=GBmaster',
);
await expect(result.files[0].content()).resolves.toEqual(
Buffer.from('# Test\n'),
);
});
it('throws NotModifiedError when same etag', async () => {
await expect(
processor.search(
'https://dev.azure.com/org-name/project-name/_git/repo-name?path=**/index.*&version=GBmaster',
{ etag: '123abc2' },
),
).rejects.toThrow(NotModifiedError);
});
});
});
@@ -15,38 +15,41 @@
*/
import {
AzureIntegrationConfig,
readAzureIntegrationConfigs,
getAzureFileFetchUrl,
getAzureDownloadUrl,
getAzureRequestOptions,
AzureIntegration,
getAzureCommitsUrl,
getAzureDownloadUrl,
getAzureFileFetchUrl,
getAzureRequestOptions,
ScmIntegrations,
} from '@backstage/integration';
import fetch from 'cross-fetch';
import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
import { NotFoundError, NotModifiedError } from '../errors';
import { ReadTreeResponseFactory } from './tree';
import { stripFirstDirectoryFromPath } from './tree/util';
import {
ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
SearchOptions,
SearchResponse,
UrlReader,
} from './types';
import { ReadTreeResponseFactory } from './tree';
export class AzureUrlReader implements UrlReader {
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
const configs = readAzureIntegrationConfigs(
config.getOptionalConfigArray('integrations.azure') ?? [],
);
return configs.map(options => {
const reader = new AzureUrlReader(options, { treeResponseFactory });
const predicate = (url: URL) => url.host === options.host;
const integrations = ScmIntegrations.fromConfig(config);
return integrations.azure.list().map(integration => {
const reader = new AzureUrlReader(integration, { treeResponseFactory });
const predicate = (url: URL) => url.host === integration.config.host;
return { reader, predicate };
});
};
constructor(
private readonly options: AzureIntegrationConfig,
private readonly integration: AzureIntegration,
private readonly deps: { treeResponseFactory: ReadTreeResponseFactory },
) {}
@@ -55,7 +58,10 @@ export class AzureUrlReader implements UrlReader {
let response: Response;
try {
response = await fetch(builtUrl, getAzureRequestOptions(this.options));
response = await fetch(
builtUrl,
getAzureRequestOptions(this.integration.config),
);
} catch (e) {
throw new Error(`Unable to read ${url}, ${e}`);
}
@@ -82,7 +88,7 @@ export class AzureUrlReader implements UrlReader {
const commitsAzureResponse = await fetch(
getAzureCommitsUrl(url),
getAzureRequestOptions(this.options),
getAzureRequestOptions(this.integration.config),
);
if (!commitsAzureResponse.ok) {
const message = `Failed to read tree from ${url}, ${commitsAzureResponse.status} ${commitsAzureResponse.statusText}`;
@@ -99,7 +105,9 @@ export class AzureUrlReader implements UrlReader {
const archiveAzureResponse = await fetch(
getAzureDownloadUrl(url),
getAzureRequestOptions(this.options, { Accept: 'application/zip' }),
getAzureRequestOptions(this.integration.config, {
Accept: 'application/zip',
}),
);
if (!archiveAzureResponse.ok) {
const message = `Failed to read tree from ${url}, ${archiveAzureResponse.status} ${archiveAzureResponse.statusText}`;
@@ -116,8 +124,38 @@ export class AzureUrlReader implements UrlReader {
});
}
async search(url: string, options?: SearchOptions): Promise<SearchResponse> {
const { filepath } = parseGitUrl(url);
const matcher = new Minimatch(filepath);
// TODO(freben): For now, read the entire repo and filter through that. In
// a future improvement, we could be smart and try to deduce that non-glob
// prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used
// to get just that part of the repo.
const treeUrl = new URL(url);
treeUrl.searchParams.delete('path');
treeUrl.pathname = treeUrl.pathname.replace(/\/+$/, '');
const tree = await this.readTree(treeUrl.toString(), {
etag: options?.etag,
filter: path => matcher.match(stripFirstDirectoryFromPath(path)),
});
const files = await tree.files();
return {
etag: tree.etag,
files: files.map(file => ({
url: this.integration.resolveUrl({
url: `/${file.path}`,
base: url,
}),
content: file.content,
})),
};
}
toString() {
const { host, token } = this.options;
const { host, token } = this.integration.config;
return `azure{host=${host},authed=${Boolean(token)}}`;
}
}
@@ -15,11 +15,16 @@
*/
import { ConfigReader } from '@backstage/config';
import {
BitbucketIntegration,
readBitbucketIntegrationConfig,
} from '@backstage/integration';
import { msw } from '@backstage/test-utils';
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import os from 'os';
import path from 'path';
import { NotModifiedError } from '../errors';
import { BitbucketUrlReader } from './BitbucketUrlReader';
@@ -30,19 +35,45 @@ const treeResponseFactory = ReadTreeResponseFactory.create({
});
const bitbucketProcessor = new BitbucketUrlReader(
{ host: 'bitbucket.org', apiBaseUrl: 'https://api.bitbucket.org/2.0' },
new BitbucketIntegration(
readBitbucketIntegrationConfig(
new ConfigReader({
host: 'bitbucket.org',
apiBaseUrl: 'https://api.bitbucket.org/2.0',
}),
),
),
{ treeResponseFactory },
);
const hostedBitbucketProcessor = new BitbucketUrlReader(
{
host: 'bitbucket.mycompany.net',
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
},
new BitbucketIntegration(
readBitbucketIntegrationConfig(
new ConfigReader({
host: 'bitbucket.mycompany.net',
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
}),
),
),
{ treeResponseFactory },
);
const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
describe('BitbucketUrlReader', () => {
beforeEach(() => {
mockFs({
[tmpDir]: mockFs.directory(),
});
});
afterEach(() => {
mockFs.restore();
});
const worker = setupServer();
msw.setupDefaultHandlers(worker);
describe('implementation', () => {
it('rejects unknown targets', async () => {
await expect(
@@ -54,19 +85,6 @@ describe('BitbucketUrlReader', () => {
});
describe('readTree', () => {
beforeEach(() => {
mockFs({
'/tmp': mockFs.directory(),
});
});
afterEach(() => {
mockFs.restore();
});
const worker = setupServer();
msw.setupDefaultHandlers(worker);
const repoBuffer = fs.readFileSync(
path.resolve(
'src',
@@ -247,12 +265,153 @@ describe('BitbucketUrlReader', () => {
expect(() => {
/* eslint-disable no-new */
new BitbucketUrlReader(
{
host: 'bitbucket.mycompany.net',
},
new BitbucketIntegration(
readBitbucketIntegrationConfig(
new ConfigReader({
host: 'bitbucket.mycompany.net',
}),
),
),
{ treeResponseFactory },
);
}).toThrowError('must configure an explicit apiBaseUrl');
});
});
describe('search hosted', () => {
const repoBuffer = fs.readFileSync(
path.resolve(
'src',
'reading',
'__fixtures__',
'bitbucket-repo-with-commit-hash.zip',
),
);
beforeEach(() => {
worker.use(
rest.get(
'https://api.bitbucket.org/2.0/repositories/backstage/mock',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
mainbranch: {
type: 'branch',
name: 'master',
},
}),
),
),
rest.get(
'https://bitbucket.org/backstage/mock/get/master.zip',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/zip'),
ctx.set(
'content-disposition',
'attachment; filename=backstage-mock-12ab34cd56ef.zip',
),
ctx.body(repoBuffer),
),
),
rest.get(
'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
}),
),
),
);
});
it('works for the naive case', async () => {
const result = await bitbucketProcessor.search(
'https://bitbucket.org/backstage/mock/src/master/**/index.*',
);
expect(result.etag).toBe('12ab34cd56ef');
expect(result.files.length).toBe(1);
expect(result.files[0].url).toBe(
'https://bitbucket.org/backstage/mock/src/master/docs/index.md',
);
await expect(result.files[0].content()).resolves.toEqual(
Buffer.from('# Test\n'),
);
});
it('throws NotModifiedError when same etag', async () => {
await expect(
bitbucketProcessor.search(
'https://bitbucket.org/backstage/mock/src/master/**/index.*',
{ etag: '12ab34cd56ef' },
),
).rejects.toThrow(NotModifiedError);
});
});
describe('search private', () => {
const privateBitbucketRepoBuffer = fs.readFileSync(
path.resolve(
'src',
'reading',
'__fixtures__',
'bitbucket-server-repo.zip',
),
);
beforeEach(() => {
worker.use(
rest.get(
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=zip&prefix=mock&path=docs',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/zip'),
ctx.set(
'content-disposition',
'attachment; filename=backstage-mock.zip',
),
ctx.body(privateBitbucketRepoBuffer),
),
),
rest.get(
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }],
}),
),
),
);
});
it('works for the naive case', async () => {
const result = await hostedBitbucketProcessor.search(
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master',
);
expect(result.etag).toBe('12ab34cd56ef');
expect(result.files.length).toBe(1);
expect(result.files[0].url).toBe(
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master',
);
await expect(result.files[0].content()).resolves.toEqual(
Buffer.from('# Test\n'),
);
});
it('throws NotModifiedError when same etag', async () => {
await expect(
hostedBitbucketProcessor.search(
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master',
{ etag: '12ab34cd56ef' },
),
).rejects.toThrow(NotModifiedError);
});
});
});
@@ -15,22 +15,26 @@
*/
import {
BitbucketIntegrationConfig,
BitbucketIntegration,
getBitbucketDefaultBranch,
getBitbucketDownloadUrl,
getBitbucketFileFetchUrl,
getBitbucketRequestOptions,
readBitbucketIntegrationConfigs,
ScmIntegrations,
} from '@backstage/integration';
import fetch from 'cross-fetch';
import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
import { NotFoundError, NotModifiedError } from '../errors';
import { ReadTreeResponseFactory } from './tree';
import { stripFirstDirectoryFromPath } from './tree/util';
import {
ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
SearchOptions,
SearchResponse,
UrlReader,
} from './types';
@@ -39,45 +43,43 @@ import {
* the one exposed by Bitbucket Cloud itself.
*/
export class BitbucketUrlReader implements UrlReader {
private readonly config: BitbucketIntegrationConfig;
private readonly treeResponseFactory: ReadTreeResponseFactory;
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
const configs = readBitbucketIntegrationConfigs(
config.getOptionalConfigArray('integrations.bitbucket') ?? [],
);
return configs.map(provider => {
const reader = new BitbucketUrlReader(provider, { treeResponseFactory });
const predicate = (url: URL) => url.host === provider.host;
const integrations = ScmIntegrations.fromConfig(config);
return integrations.bitbucket.list().map(integration => {
const reader = new BitbucketUrlReader(integration, {
treeResponseFactory,
});
const predicate = (url: URL) => url.host === integration.config.host;
return { reader, predicate };
});
};
constructor(
config: BitbucketIntegrationConfig,
deps: { treeResponseFactory: ReadTreeResponseFactory },
private readonly integration: BitbucketIntegration,
private readonly deps: { treeResponseFactory: ReadTreeResponseFactory },
) {
const { host, apiBaseUrl, token, username, appPassword } = config;
const {
host,
apiBaseUrl,
token,
username,
appPassword,
} = integration.config;
if (!apiBaseUrl) {
throw new Error(
`Bitbucket integration for '${host}' must configure an explicit apiBaseUrl`,
);
}
if (!token && username && !appPassword) {
} else if (!token && username && !appPassword) {
throw new Error(
`Bitbucket integration for '${host}' has configured a username but is missing a required appPassword.`,
);
}
this.config = config;
this.treeResponseFactory = deps.treeResponseFactory;
}
async read(url: string): Promise<Buffer> {
const bitbucketUrl = getBitbucketFileFetchUrl(url, this.config);
const options = getBitbucketRequestOptions(this.config);
const bitbucketUrl = getBitbucketFileFetchUrl(url, this.integration.config);
const options = getBitbucketRequestOptions(this.integration.config);
let response: Response;
try {
@@ -108,10 +110,13 @@ export class BitbucketUrlReader implements UrlReader {
throw new NotModifiedError();
}
const downloadUrl = await getBitbucketDownloadUrl(url, this.config);
const downloadUrl = await getBitbucketDownloadUrl(
url,
this.integration.config,
);
const archiveBitbucketResponse = await fetch(
downloadUrl,
getBitbucketRequestOptions(this.config),
getBitbucketRequestOptions(this.integration.config),
);
if (!archiveBitbucketResponse.ok) {
const message = `Failed to read tree from ${url}, ${archiveBitbucketResponse.status} ${archiveBitbucketResponse.statusText}`;
@@ -121,7 +126,7 @@ export class BitbucketUrlReader implements UrlReader {
throw new Error(message);
}
return await this.treeResponseFactory.fromZipArchive({
return await this.deps.treeResponseFactory.fromZipArchive({
stream: (archiveBitbucketResponse.body as unknown) as Readable,
subpath: filepath,
etag: lastCommitShortHash,
@@ -129,8 +134,36 @@ export class BitbucketUrlReader implements UrlReader {
});
}
async search(url: string, options?: SearchOptions): Promise<SearchResponse> {
const { filepath } = parseGitUrl(url);
const matcher = new Minimatch(filepath);
// TODO(freben): For now, read the entire repo and filter through that. In
// a future improvement, we could be smart and try to deduce that non-glob
// prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used
// to get just that part of the repo.
const treeUrl = url.replace(filepath, '').replace(/\/+$/, '');
const tree = await this.readTree(treeUrl, {
etag: options?.etag,
filter: path => matcher.match(stripFirstDirectoryFromPath(path)),
});
const files = await tree.files();
return {
etag: tree.etag,
files: files.map(file => ({
url: this.integration.resolveUrl({
url: `/${file.path}`,
base: url,
}),
content: file.content,
})),
};
}
toString() {
const { host, token, username, appPassword } = this.config;
const { host, token, username, appPassword } = this.integration.config;
let authed = Boolean(token);
if (!authed) {
authed = Boolean(username && appPassword);
@@ -143,18 +176,18 @@ export class BitbucketUrlReader implements UrlReader {
let branch = ref;
if (!branch) {
branch = await getBitbucketDefaultBranch(url, this.config);
branch = await getBitbucketDefaultBranch(url, this.integration.config);
}
const isHosted = resource === 'bitbucket.org';
// Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp222
const commitsApiUrl = isHosted
? `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`
: `${this.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`;
? `${this.integration.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`
: `${this.integration.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`;
const commitsResponse = await fetch(
commitsApiUrl,
getBitbucketRequestOptions(this.config),
getBitbucketRequestOptions(this.integration.config),
);
if (!commitsResponse.ok) {
const message = `Failed to retrieve commits from ${commitsApiUrl}, ${commitsResponse.status} ${commitsResponse.statusText}`;
@@ -16,7 +16,12 @@
import fetch from 'cross-fetch';
import { NotFoundError } from '../errors';
import { ReaderFactory, ReadTreeResponse, UrlReader } from './types';
import {
ReaderFactory,
ReadTreeResponse,
SearchResponse,
UrlReader,
} from './types';
/**
* A UrlReader that does a plain fetch of the URL.
@@ -68,10 +73,14 @@ export class FetchUrlReader implements UrlReader {
throw new Error(message);
}
readTree(): Promise<ReadTreeResponse> {
async readTree(): Promise<ReadTreeResponse> {
throw new Error('FetchUrlReader does not implement readTree');
}
async search(): Promise<SearchResponse> {
throw new Error('FetchUrlReader does not implement search');
}
toString() {
return 'fetch{}';
}
@@ -15,15 +15,26 @@
*/
import { ConfigReader } from '@backstage/config';
import { GithubCredentialsProvider } from '@backstage/integration';
import {
GithubCredentialsProvider,
GitHubIntegration,
readGitHubIntegrationConfig,
} from '@backstage/integration';
import { msw } from '@backstage/test-utils';
import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import os from 'os';
import path from 'path';
import { NotFoundError, NotModifiedError } from '../errors';
import { GithubUrlReader } from './GithubUrlReader';
import {
GhBlobResponse,
GhBranchResponse,
GhRepoResponse,
GhTreeResponse,
GithubUrlReader,
} from './GithubUrlReader';
import { ReadTreeResponseFactory } from './tree';
const treeResponseFactory = ReadTreeResponseFactory.create({
@@ -35,26 +46,45 @@ const mockCredentialsProvider = ({
} as unknown) as GithubCredentialsProvider;
const githubProcessor = new GithubUrlReader(
{
host: 'github.com',
apiBaseUrl: 'https://api.github.com',
},
new GitHubIntegration(
readGitHubIntegrationConfig(
new ConfigReader({
host: 'github.com',
apiBaseUrl: 'https://api.github.com',
}),
),
),
{ treeResponseFactory, credentialsProvider: mockCredentialsProvider },
);
const gheProcessor = new GithubUrlReader(
{
host: 'ghe.github.com',
apiBaseUrl: 'https://ghe.github.com/api/v3',
},
new GitHubIntegration(
readGitHubIntegrationConfig(
new ConfigReader({
host: 'ghe.github.com',
apiBaseUrl: 'https://ghe.github.com/api/v3',
}),
),
),
{ treeResponseFactory, credentialsProvider: mockCredentialsProvider },
);
const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
describe('GithubUrlReader', () => {
const worker = setupServer();
msw.setupDefaultHandlers(worker);
beforeEach(() => {
mockFs({
[tmpDir]: mockFs.directory(),
});
});
afterEach(() => {
mockFs.restore();
});
beforeEach(() => {
jest.clearAllMocks();
});
@@ -69,6 +99,10 @@ describe('GithubUrlReader', () => {
});
});
/*
* read
*/
describe('read', () => {
it('should use the headers from the credentials provider to the fetch request when doing read', async () => {
expect.assertions(2);
@@ -84,7 +118,7 @@ describe('GithubUrlReader', () => {
worker.use(
rest.get(
'https://api.github.com/repos/backstage/mock/tree/contents/?ref=main',
'https://ghe.github.com/api/v3/repos/backstage/mock/tree/contents/?ref=main',
(req, res, ctx) => {
expect(req.headers.get('authorization')).toBe(
mockHeaders.Authorization,
@@ -101,23 +135,17 @@ describe('GithubUrlReader', () => {
),
);
await githubProcessor.read(
await gheProcessor.read(
'https://github.com/backstage/mock/tree/blob/main',
);
});
});
/*
* readTree
*/
describe('readTree', () => {
beforeEach(() => {
mockFs({
'/tmp': mockFs.directory(),
});
});
afterEach(() => {
mockFs.restore();
});
const repoBuffer = fs.readFileSync(
path.resolve(
'src',
@@ -128,29 +156,31 @@ describe('GithubUrlReader', () => {
);
const reposGithubApiResponse = {
id: '123',
id: 123,
full_name: 'backstage/mock',
default_branch: 'main',
branches_url:
'https://api.github.com/repos/backstage/mock/branches{/branch}',
archive_url:
'https://api.github.com/repos/backstage/mock/{archive_format}{/ref}',
};
} as Partial<GhRepoResponse>;
const reposGheApiResponse = {
...reposGithubApiResponse,
id: 123,
full_name: 'backstage/mock',
default_branch: 'main',
branches_url:
'https://ghe.github.com/api/v3/repos/backstage/mock/branches{/branch}',
archive_url:
'https://ghe.github.com/api/v3/repos/backstage/mock/{archive_format}{/ref}',
};
} as Partial<GhRepoResponse>;
const branchesApiResponse = {
name: 'main',
commit: {
sha: 'etag123abc',
},
};
} as Partial<GhBranchResponse>;
beforeEach(() => {
worker.use(
@@ -381,9 +411,13 @@ describe('GithubUrlReader', () => {
expect(() => {
/* eslint-disable no-new */
new GithubUrlReader(
{
host: 'ghe.mycompany.net',
},
new GitHubIntegration(
readGitHubIntegrationConfig(
new ConfigReader({
host: 'ghe.mycompany.net',
}),
),
),
{
treeResponseFactory,
credentialsProvider: mockCredentialsProvider,
@@ -392,4 +426,351 @@ describe('GithubUrlReader', () => {
}).toThrowError('must configure an explicit apiBaseUrl');
});
});
/*
* search
*/
describe('search', () => {
const repoBuffer = fs.readFileSync(
path.resolve(
'src',
'reading',
'__fixtures__',
'backstage-mock-etag123.tar.gz',
),
);
const githubTreeContents: GhTreeResponse['tree'] = [
{
path: 'mkdocs.yml',
type: 'blob',
url: 'https://api.github.com/repos/backstage/mock/git/blobs/1',
},
{
path: 'docs',
type: 'tree',
url: 'https://api.github.com/repos/backstage/mock/git/trees/2',
},
{
path: 'docs/index.md',
type: 'blob',
url: 'https://api.github.com/repos/backstage/mock/git/blobs/3',
},
];
const gheTreeContents: GhTreeResponse['tree'] = [
{
path: 'mkdocs.yml',
type: 'blob',
url: 'https://ghe.github.com/api/v3/repos/backstage/mock/git/blobs/1',
},
{
path: 'docs',
type: 'tree',
url: 'https://ghe.github.com/api/v3/repos/backstage/mock/git/trees/2',
},
{
path: 'docs/index.md',
type: 'blob',
url: 'https://ghe.github.com/api/v3/repos/backstage/mock/git/blobs/3',
},
];
// Tarballs
beforeEach(() => {
worker.use(
rest.get(
'https://api.github.com/repos/backstage/mock/tarball/etag123abc',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/x-gzip'),
ctx.set(
'content-disposition',
'attachment; filename=backstage-mock-etag123.tar.gz',
),
ctx.body(repoBuffer),
),
),
rest.get(
'https://ghe.github.com/api/v3/repos/backstage/mock/tarball/etag123abc',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/x-gzip'),
ctx.set(
'content-disposition',
'attachment; filename=backstage-mock-etag123.tar.gz',
),
ctx.body(repoBuffer),
),
),
);
});
// Repo details
beforeEach(() => {
const githubResponse = {
id: 123,
full_name: 'backstage/mock',
default_branch: 'main',
branches_url:
'https://api.github.com/repos/backstage/mock/branches{/branch}',
archive_url:
'https://api.github.com/repos/backstage/mock/{archive_format}{/ref}',
trees_url:
'https://api.github.com/repos/backstage/mock/git/trees{/sha}',
} as Partial<GhRepoResponse>;
const gheResponse = {
id: 123,
full_name: 'backstage/mock',
default_branch: 'main',
branches_url:
'https://ghe.github.com/api/v3/repos/backstage/mock/branches{/branch}',
archive_url:
'https://ghe.github.com/api/v3/repos/backstage/mock/{archive_format}{/ref}',
trees_url:
'https://ghe.github.com/api/v3/repos/backstage/mock/git/trees{/sha}',
} as Partial<GhRepoResponse>;
worker.use(
rest.get('https://api.github.com/repos/backstage/mock', (_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(githubResponse),
),
),
rest.get(
'https://ghe.github.com/api/v3/repos/backstage/mock',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(gheResponse),
),
),
);
});
// Branch details
beforeEach(() => {
const response = {
name: 'main',
commit: {
sha: 'etag123abc',
},
} as Partial<GhBranchResponse>;
worker.use(
rest.get(
'https://api.github.com/repos/backstage/mock/branches/main',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(response),
),
),
rest.get(
'https://ghe.github.com/api/v3/repos/backstage/mock/branches/main',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(response),
),
),
rest.get(
'https://api.github.com/repos/backstage/mock/branches/branchDoesNotExist',
(_, res, ctx) => res(ctx.status(404)),
),
);
});
// Blobs
beforeEach(() => {
const blob1Response = {
content: Buffer.from('site_name: Test\n').toString('base64'),
} as Partial<GhBlobResponse>;
const blob3Response = {
content: Buffer.from('# Test\n').toString('base64'),
} as Partial<GhBlobResponse>;
worker.use(
rest.get(
'https://api.github.com/repos/backstage/mock/git/blobs/1',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(blob1Response),
),
),
rest.get(
'https://api.github.com/repos/backstage/mock/git/blobs/3',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(blob3Response),
),
),
rest.get(
'https://ghe.github.com/api/v3/repos/backstage/mock/git/blobs/1',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(blob1Response),
),
),
rest.get(
'https://ghe.github.com/api/v3/repos/backstage/mock/git/blobs/3',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(blob3Response),
),
),
);
});
async function runTests(reader: GithubUrlReader, baseUrl: string) {
const r1 = await reader.search(
`${baseUrl}/backstage/mock/tree/main/**/*`,
);
expect(r1.etag).toBe('etag123abc');
expect(r1.files.length).toBe(2);
const r2 = await reader.search(
`${baseUrl}/backstage/mock/tree/main/**/*`,
{ etag: 'somethingElse' },
);
expect(r2.etag).toBe('etag123abc');
expect(r2.files.length).toBe(2);
const r3 = await reader.search(`${baseUrl}/backstage/mock/tree/main/o`);
expect(r3.files.length).toBe(0);
const r4 = await reader.search(
`${baseUrl}/backstage/mock/tree/main/*docs*`,
);
expect(r4.files.length).toBe(1);
expect(r4.files[0].url).toBe(
`${baseUrl}/backstage/mock/tree/main/mkdocs.yml`,
);
await expect(r4.files[0].content()).resolves.toEqual(
Buffer.from('site_name: Test\n'),
);
const r5 = await reader.search(
`${baseUrl}/backstage/mock/tree/main/*/index.*`,
);
expect(r5.files.length).toBe(1);
expect(r5.files[0].url).toBe(
`${baseUrl}/backstage/mock/tree/main/docs/index.md`,
);
await expect(r5.files[0].content()).resolves.toEqual(
Buffer.from('# Test\n'),
);
}
// eslint-disable-next-line jest/expect-expect
it('succeeds on github when going via repo listing', async () => {
worker.use(
rest.get(
'https://api.github.com/repos/backstage/mock/git/trees/etag123abc',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json({
truncated: false,
tree: githubTreeContents,
} as Partial<GhTreeResponse>),
),
),
);
await runTests(githubProcessor, 'https://github.com');
});
// eslint-disable-next-line jest/expect-expect
it('succeeds on github when going via readTree', async () => {
worker.use(
rest.get(
'https://api.github.com/repos/backstage/mock/git/trees/etag123abc',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json({
truncated: true,
tree: [],
} as Partial<GhTreeResponse>),
),
),
);
await runTests(githubProcessor, 'https://github.com');
});
// eslint-disable-next-line jest/expect-expect
it('succeeds on ghe when going via repo listing', async () => {
worker.use(
rest.get(
'https://ghe.github.com/api/v3/repos/backstage/mock/git/trees/etag123abc',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json({
truncated: false,
tree: gheTreeContents,
} as Partial<GhTreeResponse>),
),
),
);
await runTests(gheProcessor, 'https://ghe.github.com');
});
// eslint-disable-next-line jest/expect-expect
it('succeeds on ghe when going via readTree', async () => {
worker.use(
rest.get(
'https://ghe.github.com/api/v3/repos/backstage/mock/git/trees/etag123abc',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json({
truncated: true,
tree: [],
} as Partial<GhTreeResponse>),
),
),
);
await runTests(gheProcessor, 'https://ghe.github.com');
});
it('throws NotModifiedError when same etag', async () => {
await expect(
githubProcessor.search(
'https://githib.com/backstage/mock/tree/main/**/*',
{ etag: 'etag123abc' },
),
).rejects.toThrow(NotModifiedError);
});
it('throws NotFoundError when missing branch', async () => {
await expect(
githubProcessor.search(
'https://githib.com/backstage/mock/tree/branchDoesNotExist/**/*',
),
).rejects.toThrow(NotFoundError);
});
});
});
@@ -17,12 +17,13 @@
import {
getGitHubFileFetchUrl,
GithubCredentialsProvider,
GitHubIntegrationConfig,
readGitHubIntegrationConfigs,
GitHubIntegration,
ScmIntegrations,
} from '@backstage/integration';
import { RestEndpointMethodTypes } from '@octokit/rest';
import fetch from 'cross-fetch';
import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
import { NotFoundError, NotModifiedError } from '../errors';
import { ReadTreeResponseFactory } from './tree';
@@ -30,45 +31,53 @@ import {
ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
SearchOptions,
SearchResponse,
SearchResponseFile,
UrlReader,
} from './types';
export type GhRepoResponse = RestEndpointMethodTypes['repos']['get']['response']['data'];
export type GhBranchResponse = RestEndpointMethodTypes['repos']['getBranch']['response']['data'];
export type GhTreeResponse = RestEndpointMethodTypes['git']['getTree']['response']['data'];
export type GhBlobResponse = RestEndpointMethodTypes['git']['getBlob']['response']['data'];
/**
* A processor that adds the ability to read files from GitHub v3 APIs, such as
* the one exposed by GitHub itself.
*/
export class GithubUrlReader implements UrlReader {
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
const configs = readGitHubIntegrationConfigs(
config.getOptionalConfigArray('integrations.github') ?? [],
);
return configs.map(provider => {
const credentialsProvider = GithubCredentialsProvider.create(provider);
const reader = new GithubUrlReader(provider, {
const integrations = ScmIntegrations.fromConfig(config);
return integrations.github.list().map(integration => {
const credentialsProvider = GithubCredentialsProvider.create(
integration.config,
);
const reader = new GithubUrlReader(integration, {
treeResponseFactory,
credentialsProvider,
});
const predicate = (url: URL) => url.host === provider.host;
const predicate = (url: URL) => url.host === integration.config.host;
return { reader, predicate };
});
};
constructor(
private readonly config: GitHubIntegrationConfig,
private readonly integration: GitHubIntegration,
private readonly deps: {
treeResponseFactory: ReadTreeResponseFactory;
credentialsProvider: GithubCredentialsProvider;
},
) {
if (!config.apiBaseUrl && !config.rawBaseUrl) {
if (!integration.config.apiBaseUrl && !integration.config.rawBaseUrl) {
throw new Error(
`GitHub integration for '${config.host}' must configure an explicit apiBaseUrl and rawBaseUrl`,
`GitHub integration '${integration.title}' must configure an explicit apiBaseUrl or rawBaseUrl`,
);
}
}
async read(url: string): Promise<Buffer> {
const ghUrl = getGitHubFileFetchUrl(url, this.config);
const ghUrl = getGitHubFileFetchUrl(url, this.integration.config);
const { headers } = await this.deps.credentialsProvider.getCredentials({
url,
});
@@ -106,39 +115,136 @@ export class GithubUrlReader implements UrlReader {
throw new NotModifiedError();
}
const { filepath } = parseGitUrl(url);
const { headers } = await this.deps.credentialsProvider.getCredentials({
url,
});
// archive_url looks like "https://api.github.com/repos/owner/repo/{archive_format}{/ref}"
const archive = await this.fetchResponse(
repoDetails.repo.archive_url
.replace('{archive_format}', 'tarball')
.replace('{/ref}', `/${commitSha}`),
return this.doReadTree(
repoDetails.repo.archive_url,
commitSha,
filepath,
{ headers },
options,
);
}
async search(url: string, options?: SearchOptions): Promise<SearchResponse> {
const repoDetails = await this.getRepoDetails(url);
const commitSha = repoDetails.branch.commit.sha!;
if (options?.etag && options.etag === commitSha) {
throw new NotModifiedError();
}
const { filepath } = parseGitUrl(url);
const { headers } = await this.deps.credentialsProvider.getCredentials({
url,
});
const files = await this.doSearch(
url,
repoDetails.repo.trees_url,
repoDetails.repo.archive_url,
commitSha,
filepath,
{ headers },
);
const { filepath } = parseGitUrl(url);
return { files, etag: commitSha };
}
toString() {
const { host, token } = this.integration.config;
return `github{host=${host},authed=${Boolean(token)}}`;
}
private async doReadTree(
archiveUrl: string,
sha: string,
subpath: string,
init: RequestInit,
options?: ReadTreeOptions,
): Promise<ReadTreeResponse> {
// archive_url looks like "https://api.github.com/repos/owner/repo/{archive_format}{/ref}"
const archive = await this.fetchResponse(
archiveUrl
.replace('{archive_format}', 'tarball')
.replace('{/ref}', `/${sha}`),
init,
);
return await this.deps.treeResponseFactory.fromTarArchive({
// TODO(Rugvip): Underlying implementation of fetch will be node-fetch, we probably want
// to stick to using that in exclusively backend code.
stream: (archive.body as unknown) as Readable,
subpath: filepath,
etag: commitSha,
subpath,
etag: sha,
filter: options?.filter,
});
}
toString() {
const { host, token } = this.config;
return `github{host=${host},authed=${Boolean(token)}}`;
private async doSearch(
url: string,
treesUrl: string,
archiveUrl: string,
sha: string,
query: string,
init: RequestInit,
): Promise<SearchResponseFile[]> {
function pathToUrl(path: string): string {
// TODO(freben): Use the integration package facility for this instead
// pathname starts as /backstage/backstage/blob/master/<path>
const updated = new URL(url);
const base = updated.pathname.split('/').slice(1, 5).join('/');
updated.pathname = `${base}/${path}`;
return updated.toString();
}
const matcher = new Minimatch(query.replace(/^\/+/, ''));
// trees_url looks like "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}"
const recursiveTree: GhTreeResponse = await this.fetchJson(
treesUrl.replace('{/sha}', `/${sha}?recursive=true`),
init,
);
// The simple case is that we got the entire tree in a single operation.
if (!recursiveTree.truncated) {
const matching = recursiveTree.tree.filter(
item =>
item.type === 'blob' &&
item.path &&
item.url &&
matcher.match(item.path),
);
return matching.map(item => ({
url: pathToUrl(item.path!),
content: async () => {
const blob: GhBlobResponse = await this.fetchJson(item.url!, init);
return Buffer.from(blob.content, 'base64');
},
}));
}
// For larger repos, we leverage readTree and filter through that instead
const tree = await this.doReadTree(archiveUrl, sha, '', init, {
filter: path => matcher.match(path),
});
const files = await tree.files();
return files.map(file => ({
url: pathToUrl(file.path),
content: file.content,
}));
}
private async getRepoDetails(
url: string,
): Promise<{
repo: RestEndpointMethodTypes['repos']['get']['response']['data'];
branch: RestEndpointMethodTypes['repos']['getBranch']['response']['data'];
repo: GhRepoResponse;
branch: GhBranchResponse;
}> {
const parsed = parseGitUrl(url);
const { ref, full_name } = parsed;
@@ -151,13 +257,13 @@ export class GithubUrlReader implements UrlReader {
url,
});
const repo: RestEndpointMethodTypes['repos']['get']['response']['data'] = await this.fetchJson(
`${this.config.apiBaseUrl}/repos/${full_name}`,
const repo: GhRepoResponse = await this.fetchJson(
`${this.integration.config.apiBaseUrl}/repos/${full_name}`,
{ headers },
);
// branches_url looks like "https://api.github.com/repos/owner/repo/branches{/branch}"
const branch: RestEndpointMethodTypes['repos']['getBranch']['response']['data'] = await this.fetchJson(
const branch: GhBranchResponse = await this.fetchJson(
repo.branches_url.replace('{/branch}', `/${ref || repo.default_branch}`),
{ headers },
);
@@ -20,11 +20,16 @@ import fs from 'fs-extra';
import mockFs from 'mock-fs';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import os from 'os';
import path from 'path';
import { getVoidLogger } from '../logging';
import { GitlabUrlReader } from './GitlabUrlReader';
import { ReadTreeResponseFactory } from './tree';
import { NotModifiedError, NotFoundError } from '../errors';
import {
GitLabIntegration,
readGitLabIntegrationConfig,
} from '@backstage/integration';
const logger = getVoidLogger();
@@ -33,22 +38,44 @@ const treeResponseFactory = ReadTreeResponseFactory.create({
});
const gitlabProcessor = new GitlabUrlReader(
{
host: 'gitlab.com',
apiBaseUrl: 'https://gitlab.com/api/v4',
},
new GitLabIntegration(
readGitLabIntegrationConfig(
new ConfigReader({
host: 'gitlab.com',
apiBaseUrl: 'https://gitlab.com/api/v4',
baseUrl: 'https://gitlab.com',
}),
),
),
{ treeResponseFactory },
);
const hostedGitlabProcessor = new GitlabUrlReader(
{
host: 'gitlab.mycompany.com',
apiBaseUrl: 'https://gitlab.mycompany.com/api/v4',
},
new GitLabIntegration(
readGitLabIntegrationConfig(
new ConfigReader({
host: 'gitlab.mycompany.com',
apiBaseUrl: 'https://gitlab.mycompany.com/api/v4',
baseUrl: 'https://gitlab.mycompany.com',
}),
),
),
{ treeResponseFactory },
);
const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp';
describe('GitlabUrlReader', () => {
beforeEach(() => {
mockFs({
[tmpDir]: mockFs.directory(),
});
});
afterEach(() => {
mockFs.restore();
});
const worker = setupServer();
msw.setupDefaultHandlers(worker);
@@ -154,16 +181,6 @@ describe('GitlabUrlReader', () => {
});
describe('readTree', () => {
beforeEach(() => {
mockFs({
'/tmp': mockFs.directory(),
});
});
afterEach(() => {
mockFs.restore();
});
const archiveBuffer = fs.readFileSync(
path.resolve('src', 'reading', '__fixtures__', 'gitlab-archive.zip'),
);
@@ -379,17 +396,81 @@ describe('GitlabUrlReader', () => {
};
await expect(fnGithub).rejects.toThrow(NotFoundError);
});
});
it('should throw error when apiBaseUrl is missing', () => {
expect(() => {
/* eslint-disable no-new */
new GitlabUrlReader(
{
host: 'gitlab.mycompany.com',
},
{ treeResponseFactory },
);
}).toThrowError('must configure an explicit apiBaseUrl');
describe('search', () => {
const archiveBuffer = fs.readFileSync(
path.resolve('src', 'reading', '__fixtures__', 'gitlab-archive.zip'),
);
const projectGitlabApiResponse = {
id: 11111111,
default_branch: 'main',
};
const branchGitlabApiResponse = {
commit: {
id: 'sha123abc',
},
};
beforeEach(() => {
worker.use(
rest.get(
'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/archive.zip?sha=main',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/zip'),
ctx.set(
'content-disposition',
'attachment; filename="mock-main-sha123abc.zip"',
),
ctx.body(archiveBuffer),
),
),
rest.get(
'https://gitlab.com/api/v4/projects/backstage%2Fmock',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(projectGitlabApiResponse),
),
),
rest.get(
'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/branches/main',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(branchGitlabApiResponse),
),
),
);
});
it('works for the naive case', async () => {
const result = await gitlabProcessor.search(
'https://gitlab.com/backstage/mock/tree/main/**/index.*',
);
expect(result.etag).toBe('sha123abc');
expect(result.files.length).toBe(1);
expect(result.files[0].url).toBe(
'https://gitlab.com/backstage/mock/tree/main/docs/index.md',
);
await expect(result.files[0].content()).resolves.toEqual(
Buffer.from('# Test\n'),
);
});
it('throws NotModifiedError when same etag', async () => {
await expect(
gitlabProcessor.search(
'https://gitlab.com/backstage/mock/tree/main/**/index.*',
{ etag: 'sha123abc' },
),
).rejects.toThrow(NotModifiedError);
});
});
});
@@ -17,54 +17,51 @@
import {
getGitLabFileFetchUrl,
getGitLabRequestOptions,
GitLabIntegrationConfig,
readGitLabIntegrationConfigs,
GitLabIntegration,
ScmIntegrations,
} from '@backstage/integration';
import fetch from 'cross-fetch';
import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
import { NotFoundError, NotModifiedError } from '../errors';
import { ReadTreeResponseFactory } from './tree';
import { stripFirstDirectoryFromPath } from './tree/util';
import {
ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
SearchOptions,
SearchResponse,
UrlReader,
} from './types';
import parseGitUrl from 'git-url-parse';
import { Readable } from 'stream';
export class GitlabUrlReader implements UrlReader {
private readonly treeResponseFactory: ReadTreeResponseFactory;
static factory: ReaderFactory = ({ config, treeResponseFactory }) => {
const configs = readGitLabIntegrationConfigs(
config.getOptionalConfigArray('integrations.gitlab') ?? [],
);
return configs.map(provider => {
const reader = new GitlabUrlReader(provider, { treeResponseFactory });
const predicate = (url: URL) => url.host === provider.host;
const integrations = ScmIntegrations.fromConfig(config);
return integrations.gitlab.list().map(integration => {
const reader = new GitlabUrlReader(integration, {
treeResponseFactory,
});
const predicate = (url: URL) => url.host === integration.config.host;
return { reader, predicate };
});
};
constructor(
private readonly config: GitLabIntegrationConfig,
deps: { treeResponseFactory: ReadTreeResponseFactory },
) {
this.treeResponseFactory = deps.treeResponseFactory;
if (!config.apiBaseUrl) {
throw new Error(
`GitLab integration for '${config.host}' must configure an explicit apiBaseUrl`,
);
}
}
private readonly integration: GitLabIntegration,
private readonly deps: { treeResponseFactory: ReadTreeResponseFactory },
) {}
async read(url: string): Promise<Buffer> {
const builtUrl = await getGitLabFileFetchUrl(url, this.config);
const builtUrl = await getGitLabFileFetchUrl(url, this.integration.config);
let response: Response;
try {
response = await fetch(builtUrl, getGitLabRequestOptions(this.config));
response = await fetch(
builtUrl,
getGitLabRequestOptions(this.integration.config),
);
} catch (e) {
throw new Error(`Unable to read ${url}, ${e}`);
}
@@ -91,9 +88,11 @@ export class GitlabUrlReader implements UrlReader {
// https://docs.gitlab.com/ee/api/README.html#namespaced-path-encoding
const projectGitlabResponse = await fetch(
new URL(
`${this.config.apiBaseUrl}/projects/${encodeURIComponent(full_name)}`,
`${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent(
full_name,
)}`,
).toString(),
getGitLabRequestOptions(this.config),
getGitLabRequestOptions(this.integration.config),
);
if (!projectGitlabResponse.ok) {
const msg = `Failed to read tree from ${url}, ${projectGitlabResponse.status} ${projectGitlabResponse.statusText}`;
@@ -111,11 +110,11 @@ export class GitlabUrlReader implements UrlReader {
// the provided sha.
const branchGitlabResponse = await fetch(
new URL(
`${this.config.apiBaseUrl}/projects/${encodeURIComponent(
`${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent(
full_name,
)}/repository/branches/${branch}`,
).toString(),
getGitLabRequestOptions(this.config),
getGitLabRequestOptions(this.integration.config),
);
if (!branchGitlabResponse.ok) {
const message = `Failed to read tree (branch) from ${url}, ${branchGitlabResponse.status} ${branchGitlabResponse.statusText}`;
@@ -133,10 +132,10 @@ export class GitlabUrlReader implements UrlReader {
// https://docs.gitlab.com/ee/api/repositories.html#get-file-archive
const archiveGitLabResponse = await fetch(
`${this.config.apiBaseUrl}/projects/${encodeURIComponent(
`${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent(
full_name,
)}/repository/archive.zip?sha=${branch}`,
getGitLabRequestOptions(this.config),
getGitLabRequestOptions(this.integration.config),
);
if (!archiveGitLabResponse.ok) {
const message = `Failed to read tree (archive) from ${url}, ${archiveGitLabResponse.status} ${archiveGitLabResponse.statusText}`;
@@ -146,7 +145,7 @@ export class GitlabUrlReader implements UrlReader {
throw new Error(message);
}
return await this.treeResponseFactory.fromZipArchive({
return await this.deps.treeResponseFactory.fromZipArchive({
stream: (archiveGitLabResponse.body as unknown) as Readable,
subpath: filepath,
etag: commitSha,
@@ -154,8 +153,33 @@ export class GitlabUrlReader implements UrlReader {
});
}
async search(url: string, options?: SearchOptions): Promise<SearchResponse> {
const { filepath } = parseGitUrl(url);
const matcher = new Minimatch(filepath);
// TODO(freben): For now, read the entire repo and filter through that. In
// a future improvement, we could be smart and try to deduce that non-glob
// prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used
// to get just that part of the repo.
const treeUrl = url.replace(filepath, '').replace(/\/+$/, '');
const tree = await this.readTree(treeUrl, {
etag: options?.etag,
filter: path => matcher.match(stripFirstDirectoryFromPath(path)),
});
const files = await tree.files();
return {
etag: tree.etag,
files: files.map(file => ({
url: this.integration.resolveUrl({ url: `/${file.path}`, base: url }),
content: file.content,
})),
};
}
toString() {
const { host, token } = this.config;
const { host, token } = this.integration.config;
return `gitlab{host=${host},authed=${Boolean(token)}}`;
}
}
@@ -18,6 +18,8 @@ import { NotAllowedError } from '../errors';
import {
ReadTreeOptions,
ReadTreeResponse,
SearchOptions,
SearchResponse,
UrlReader,
UrlReaderPredicateTuple,
} from './types';
@@ -60,6 +62,18 @@ export class UrlReaderPredicateMux implements UrlReader {
throw new NotAllowedError(`Reading from '${url}' is not allowed`);
}
async search(url: string, options?: SearchOptions): Promise<SearchResponse> {
const parsed = new URL(url);
for (const { predicate, reader } of this.readers) {
if (predicate(parsed)) {
return await reader.search(url, options);
}
}
throw new NotAllowedError(`Reading from '${url}' is not allowed`);
}
toString() {
return `predicateMux{readers=${this.readers.map(t => t.reader).join(',')}`;
}
+1 -1
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
export type { UrlReader, ReadTreeResponse } from './types';
export type { UrlReader, ReadTreeResponse, SearchResponse } from './types';
export { UrlReaders } from './UrlReaders';
export { AzureUrlReader } from './AzureUrlReader';
export { BitbucketUrlReader } from './BitbucketUrlReader';
@@ -25,15 +25,12 @@ import {
ReadTreeResponseDirOptions,
ReadTreeResponseFile,
} from '../types';
import { stripFirstDirectoryFromPath } from './util';
// Tar types for `Parse` is not a proper constructor, but it should be
const TarParseStream = (Parse as unknown) as { new (): ParseStream };
const pipeline = promisify(pipelineCb);
// Matches a directory name + one `/` at the start of any string,
// containing any character except `/` one or more times, and ending with a `/`
// e.g. Will match `dirA/` in `dirA/dirB/file.ext`
const directoryNameRegex = /^[^\/]+\//;
/**
* Wraps a tar archive stream into a tree response reader.
@@ -84,7 +81,7 @@ export class TarArchiveResponse implements ReadTreeResponse {
// File path relative to the root extracted directory. Will remove the
// top level dir name from the path since its name is hard to predetermine.
const relativePath = entry.path.replace(directoryNameRegex, '');
const relativePath = stripFirstDirectoryFromPath(entry.path);
if (this.subPath) {
if (!relativePath.startsWith(this.subPath)) {
@@ -161,7 +158,7 @@ export class TarArchiveResponse implements ReadTreeResponse {
filter: path => {
// File path relative to the root extracted directory. Will remove the
// top level dir name from the path since its name is hard to predetermine.
const relativePath = path.replace(directoryNameRegex, '');
const relativePath = stripFirstDirectoryFromPath(path);
if (this.subPath && !relativePath.startsWith(this.subPath)) {
return false;
}
@@ -24,11 +24,7 @@ import {
ReadTreeResponseDirOptions,
ReadTreeResponseFile,
} from '../types';
// Matches a directory name + one `/` at the start of any string,
// containing any character except / one or more times, and ending with a `/`
// e.g. Will match `dirA/` in `dirA/dirB/file.ext`
const directoryNameRegex = /^[^\/]+\//;
import { stripFirstDirectoryFromPath } from './util';
/**
* Wraps a zip archive stream into a tree response reader.
@@ -65,18 +61,13 @@ export class ZipArchiveResponse implements ReadTreeResponse {
this.read = true;
}
// Will remove the top level dir name from the path since its name is hard to predetermine.
private stripTopDirectory(path: string): string {
return path.replace(directoryNameRegex, '');
}
// File path relative to the root extracted directory or a sub directory if subpath is set.
private getInnerPath(path: string): string {
return path.slice(this.subPath.length);
}
private shouldBeIncluded(entry: Entry): boolean {
const strippedPath = this.stripTopDirectory(entry.path);
const strippedPath = stripFirstDirectoryFromPath(entry.path);
if (this.subPath) {
if (!strippedPath.startsWith(this.subPath)) {
@@ -104,7 +95,7 @@ export class ZipArchiveResponse implements ReadTreeResponse {
if (this.shouldBeIncluded(entry)) {
files.push({
path: this.getInnerPath(this.stripTopDirectory(entry.path)),
path: this.getInnerPath(stripFirstDirectoryFromPath(entry.path)),
content: () => entry.buffer(),
});
} else {
@@ -153,7 +144,7 @@ export class ZipArchiveResponse implements ReadTreeResponse {
// as a zip can have files with directories without directory entries
if (entry.type === 'File' && this.shouldBeIncluded(entry)) {
const entryPath = this.getInnerPath(
this.stripTopDirectory(entry.path),
stripFirstDirectoryFromPath(entry.path),
);
const dirname = platformPath.dirname(entryPath);
if (dirname) {
@@ -0,0 +1,25 @@
/*
* Copyright 2021 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.
*/
// Matches a directory name + one `/` at the start of any string,
// containing any character except `/` one or more times, and ending with a `/`
// e.g. Will match `dirA/` in `dirA/dirB/file.ext`
const directoryNameRegex = /^[^\/]+\//;
// Removes the first segment of a forward-slash-separated path
export function stripFirstDirectoryFromPath(path: string): string {
return path.replace(directoryNameRegex, '');
}
@@ -24,6 +24,7 @@ import { ReadTreeResponseFactory } from './tree';
export type UrlReader = {
read(url: string): Promise<Buffer>;
readTree(url: string, options?: ReadTreeOptions): Promise<ReadTreeResponse>;
search(url: string, options?: SearchOptions): Promise<SearchResponse>;
};
export type UrlReaderPredicateTuple = {
@@ -103,3 +104,51 @@ export type ReadTreeResponseFile = {
path: string;
content(): Promise<Buffer>;
};
/**
* An options object for search operations.
*/
export type SearchOptions = {
/**
* An etag can be provided to check whether the search response has changed from a previous execution.
*
* In the search() response, an etag is returned along with the files. The etag is a unique identifer
* of the current tree, usually the commit SHA or etag from the target.
*
* When an etag is given in SearchOptions, search will first compare the etag against the etag
* on the target branch. If they match, search will throw a NotModifiedError indicating that the search
* response will not differ from the previous response which included this particular etag. If they mismatch,
* search will return the rest of SearchResponse along with a new etag.
*/
etag?: string;
};
/**
* The output of a search operation.
*/
export type SearchResponse = {
/**
* The files that matched the search query.
*/
files: SearchResponseFile[];
/**
* A unique identifer of the current remote tree, usually the commit SHA or etag from the target.
*/
etag: string;
};
/**
* Represents a single file in a search response.
*/
export type SearchResponseFile = {
/**
* The full URL to the file.
*/
url: string;
/**
* The binary contents of the file.
*/
content(): Promise<Buffer>;
};
@@ -161,7 +161,7 @@ export class ServiceBuilderImpl implements ServiceBuilder {
app.use(cors(corsOptions));
}
app.use(compression());
app.use(requestLoggingHandler());
app.use(requestLoggingHandler(logger));
for (const [root, route] of this.routers) {
app.use(root, route);
}

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