Merge branch 'master' into techdoc_title

This commit is contained in:
Aurelio Saraiva
2021-08-30 18:17:19 -03:00
582 changed files with 14919 additions and 4067 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Allow to pass custom TemplateCard to ScaffolderPage
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-catalog-react': patch
---
Added a `useEntityKinds` hook to load a unique list of entity kinds from the catalog.
Fixed a bug in `EntityTypePicker` where the component did not hide when no types were available in returned entities.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
GitHub discovery processor passes over repositories that do not have a default branch
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
GitHub create repository webhook action: `github:webhook` for Backstage plugin Scaffolder has been added.
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/plugin-techdocs': patch
'@backstage/plugin-techdocs-backend': patch
'@backstage/techdocs-common': patch
---
Adding in-context search to TechDocs Reader component. Using existing search-backend to query for indexed search results scoped into a specific entity's techdocs. Needs TechDocsCollator enabled on the backend to work.
Adding extra information to indexed tech docs documents for search.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-catalog-import': patch
---
Fix importing yaml files from URLs with trailing query parameters.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend': patch
---
Adds optional namespacing for users in the GitHub Multi Org Plugin
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/core-components': patch
'@backstage/plugin-catalog-import': patch
'@backstage/plugin-shortcuts': patch
---
Bumped `react-hook-form` to `^7.12.2`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-git-release-manager': patch
---
Wrap each feature in custom feature arrays with an element containing a 'key' to avoid missing-key-warnings
+10
View File
@@ -0,0 +1,10 @@
---
'@backstage/core-components': minor
---
Changed the `titleComponent` prop on `ContentHeader` to accept `ReactNode` instead of a React `ComponentType`. Usages of this prop should be converted from passing a component to passing in the rendered element:
```diff
-<ContentHeader titleComponent={MyComponent}>
+<ContentHeader titleComponent={<MyComponent />}>
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-org': patch
---
Don't open a new tab when clicking on the ownership card.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-xcmetrics': patch
---
Enable browsing detailed build information such as host configuration, errors, warnings, metadata and a timeline for all targets
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog': patch
---
Update the `AboutCard` to properly support non-standard entity types and rework the defaults for the build-in kinds.
This change also uses `useElementFilter(...)` instead of `React.children.count(...)` in `AboutField` to properly recognize whether children are available.
-5
View File
@@ -1,5 +0,0 @@
---
'@backstage/plugin-jenkins': patch
---
Make `isJenkinsAvailable` recognise both the old and the new Jenkins annotation.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
typo fix `tenentId` in Azure auth provider docs
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Added the ability to switch entity kind on the catalog index page. This is a non-breaking change, but if you created a custom `CatalogPage` and wish to use this feature, make the modifications shown on [#6895](https://github.com/backstage/backstage/pull/6895).
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-shortcuts': patch
---
Fix material-ui warning about deprecated `onExit` prop
+11
View File
@@ -0,0 +1,11 @@
---
'@backstage/catalog-model': patch
---
Add an optional `metadata.title` field to all entity kinds.
This used to be available on only the `Template` kind, and we have decided that the metadata block should be the same for all kinds. A title can be useful especially in large and complex catalogs where users have a tough time navigating or discerning among the entities.
It also carries some risk. You do not want to end up giving a title that collides with an actual name, which at best leads to confusion and at worst could be a liability. We do not perform any collision detection in the catalog. If you want to disallow this facility you may want to add a small processor that makes sure it's not set.
At the time of writing this message, only the scaffolder actually makes use of this field for display purposes.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': patch
---
Minor tweaks to the create-app template to match earlier documented changes
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Export type that are needed to implement a new `UrlReader`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-sonarqube': patch
---
Update README to show a simpler proxy configuration.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/techdocs-common': patch
---
Force using `posix` path for cloud storage
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog': patch
---
Use a `Link` for the edit button on the `AboutCard` instead of doing `window.open(...)`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-org': patch
---
Fix OwnershipCard links to support namespaced owners
+1
View File
@@ -17,6 +17,7 @@
/plugins/techdocs @backstage/techdocs-core
/plugins/techdocs-backend @backstage/techdocs-core
/plugins/ilert @yacut
/plugins/home @backstage/techdocs-core
/packages/search-common @backstage/techdocs-core
/packages/techdocs-common @backstage/techdocs-core
/.changeset/cost-insights-* @backstage/silver-lining
+2
View File
@@ -142,6 +142,7 @@ maintainership
makefile
md
memcache
memoize
memoized
microservice
microservices
@@ -215,6 +216,7 @@ repo
Repo
repos
rerender
rerenders
Reusability
reusability
roadmaps
@@ -5,6 +5,7 @@ on:
- '.github/workflows/chromatic-storybook-test.yml'
- 'packages/storybook/**'
- 'packages/core-components/src/**'
- '**/*.stories.tsx'
jobs:
chromatic:
+4 -1
View File
@@ -21,7 +21,7 @@
| [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go |
| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling |
| [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. |
| [Trendyol](https://trendyol.com) | [Erdogan Oksuz](https://github.com/erdoganoksuz) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. |
| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. |
| [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. |
| [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. |
| [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a teams engineering dependencies. |
@@ -42,3 +42,6 @@
| [empathy.co](https://empathy.co/) | [@guillermotti](https://github.com/guillermotti) | Developer portal for tech docs, service catalog, plugin discovery and much more. |
| [creditas.com](https://creditas.com/) | [@aureliosaraiva](https://github.com/aureliosaraiva) [@Creditas](https://github.com/creditas) | Centralization of all services, standards, documentation, etc. We started the deployment process. |
| [Prisjakt](https://www.prisjakt.nu) / [PriceSpy](https://pricespy.co.uk) | [@kennylindahl](https://github.com/kennylindahl) | Internal developer portal - Documentation, scaffolding, software catalog, TechRadar, Gitlab org data integration |
| [Powerspike](https://powerspike.tv/) | [@trelore](https://github.com/trelore) | Developer portal for documentation of core libraries and repositories. |
| [2U](https://2u.com) | [Andrew Thal](https://github.com/athal7) | Development team home-base, promoting service discoverability, resource dependencies, and tech radar |
| [Taxfix](https://taxfix.de/) | [Sami Ur Rehman](https://github.com/samiurrehman92) | Developer's portal with software catalog at it's core. Hosts API Specs, Tech Docs, Tech Radar and some custom plugins. |
+7 -3
View File
@@ -160,9 +160,13 @@ integrations:
azure:
- host: dev.azure.com
token: ${AZURE_TOKEN}
# googleGcs:
# clientEmail: 'example@example.com'
# privateKey: ${GCS_PRIVATE_KEY}
# googleGcs:
# clientEmail: 'example@example.com'
# privateKey: ${GCS_PRIVATE_KEY}
awsS3:
- host: amazonaws.com
accessKeyId: ${AWS_ACCESS_KEY_ID}
secretAccessKey: ${AWS_SECRET_ACCESS_KEY}
catalog:
rules:
+14 -1
View File
@@ -10,11 +10,16 @@ that can authenticate users using GitHub or GitHub Enterprise OAuth.
## Create an OAuth App on GitHub
To add GitHub authentication, you must create an OAuth App from the GitHub
To add GitHub authentication, you must create either a GitHub App, or an OAuth
App from the GitHub
[developer settings](https://github.com/settings/developers). The `Homepage URL`
should point to Backstage's frontend, while the `Authorization callback URL`
will point to the auth backend.
Note that if you're using a GitHub App, the allowed scopes are configured as
part of that app. This means you need to verify what scopes the plugins you use
require, so be sure to check the plugin READMEs for that information.
Settings for local development:
- Application name: Backstage (or your custom app name)
@@ -51,3 +56,11 @@ The GitHub provider is a structure with three configuration keys:
To add the provider to the frontend, add the `githubAuthApi` reference and
`SignInPage` component as shown in
[Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page).
## Difference between GitHub Apps and GitHub OAuth Apps
GitHub Apps handle OAuth scope at the app installation level, meaning that the
`scope` parameter for the call to `getAccessToken` in the frontend has no
effect. When calling `getAccessToken` in open source plugins, one should still
include the appropriate scope, but also document in the plugin README what
scopes are required for GitHub Apps.
+4 -4
View File
@@ -128,20 +128,20 @@ export default async function createPlugin({
google: createGoogleProvider({
signIn: {
resolver: async ({ profile: { email } }, ctx) => {
const [sub] = email?.split('@') ?? '';
const [id] = email?.split('@') ?? '';
// Fetch from an external system that returns entity claims like:
// ['user:default/breanna.davison', ...]
const ent = await externalSystemClient.getUsernames(email);
// Resolve group membership from the Backstage catalog
const fullEnt = await ctx.catalogIdentityClient.resolveCatalogMembership({
entityRefs: [sub].concat(ent),
entityRefs: [id].concat(ent),
logger: ctx.logger,
});
const token = await ctx.tokenIssuer.issueToken({
claims: { sub, ent: fullEnt },
claims: { sub: id, ent: fullEnt },
});
return { sub, token };
return { id, token };
},
},
}),
+1 -1
View File
@@ -44,7 +44,7 @@ The Microsoft provider is a structure with three configuration keys:
- `clientId`: Application (client) ID, found on App Registration > Overview
- `clientSecret`: Secret, found on App Registration > Certificates & secrets
- `tenentId`: Directory (tenant) ID, found on App Registration > Overview
- `tenantId`: Directory (tenant) ID, found on App Registration > Overview
## Adding the provider to the Backstage frontend
+1 -1
View File
@@ -43,7 +43,7 @@ provides decent results and performs well with ten thousands of indexed
documents. The connection to postgres is established via the database manager
also used by other plugins.
> **Important**: The search plugin requires at least Postgres 11!
> **Important**: The search plugin requires at least Postgres 12!
To use the `PgSearchEngine`, make sure that you have a Postgres database
configured and make the following changes to your backend:
@@ -34,15 +34,14 @@ export const CustomCatalogPage = ({
}: CatalogPageProps) => {
return (
<PageWithHeader title={`${orgName} Catalog`} themeId="home">
<Content>
<ContentHeader title="Components">
<CreateButton title="Create Component" to={link} />
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<EntityListProvider>
<EntityListProvider>
<Content>
<ContentHeader titleComponent={<CatalogKindHeader />}>
<CreateButton title="Create Component" to={link} />
<SupportButton>All your software catalog entities</SupportButton>
</ContentHeader>
<FilteredEntityLayout>
<FilterContainer>
<EntityKindPicker initialFilter="component" hidden />
<EntityTypePicker />
<UserListPicker initialFilter={initiallySelectedFilter} />
<EntityTagPicker />
@@ -51,8 +50,8 @@ export const CustomCatalogPage = ({
<CatalogTable columns={columns} actions={actions} />
</EntityListContainer>
</FilteredEntityLayout>
</EntityListProvider>
</Content>
</Content>
</EntityListProvider>
</PageWithHeader>
);
};
@@ -253,6 +253,22 @@ the entity belongs to the `"default"` namespace.
Namespaces may also be part of the catalog, and are `v1` / `Namespace` entities,
i.e. not Backstage specific but the same as in Kubernetes.
### `title` [optional]
A display name of the entity, to be presented in user interfaces instead of the
`name` property above, when available.
This field is sometimes useful when the `name` is cumbersome or ends up being
perceived as overly technical. The title generally does not have as stringent
format requirements on it, so it may contain special characters and be more
explanatory. Do keep it very short though, and avoid situations where a title
can be confused with the name of another entity, or where two entities share a
title.
Note that this is only for display purposes, and may be ignored by some parts of
the code. [Entity references](references.md) still always make use of the `name`
property for example, not the title.
### `description` [optional]
A human readable description of the entity, to be shown in Backstage. Should be
@@ -698,12 +714,6 @@ shape, this kind has the following structure.
Exactly equal to `backstage.io/v1beta2` and `Template`, respectively.
### `metadata.title` [required]
The nice display name for the template as a string, e.g. `React SSR Template`.
This field is required as is used to reference the template to the user instead
of the `metadata.name` field.
### `metadata.tags` [optional]
A list of strings that can be associated with the template, e.g.
@@ -325,7 +325,7 @@ spec:
output:
links:
- url: '{{steps.publish.output.remoteUrl}}'
text: 'Go to Repo'
title: 'Go to Repo'
```
## Questions?
@@ -131,6 +131,9 @@ want to have those as well as your new one, you'll need to do the following:
```ts
import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend';
import { ScmIntegrations } from '@backstage/integration';
const integrations = ScmIntegrations.fromConfig(config);
const builtInActions = createBuiltinActions({
containerRunner,
+2 -13
View File
@@ -29,7 +29,7 @@ about TechDocs and the philosophy in its
- Explore and take advantage of the large ecosystem of
[MkDocs plugins](https://www.mkdocs.org/user-guide/plugins/) to create a rich
reading experience.
- Search for and find docs (coming soon).
- Search for and find docs.
- Highlight text and raise an Issue to create feedback loop to drive quality
documentation (future).
- Contribute to and deploy from a marketplace of TechDocs widgets (future).
@@ -54,23 +54,12 @@ providers are used.
| Google Cloud Storage (GCS) | Yes ✅ |
| Amazon Web Services (AWS) S3 | Yes ✅ |
| Azure Blob Storage | Yes ✅ |
| OpenStack Swift | Yes ✅ |
| OpenStack Swift | Community ✅ |
[Reach out to us](#feedback) if you want to request more platforms.
## Project roadmap
### **Ongoing work 🚧**
**Beta release** -
[Milestone](https://github.com/backstage/backstage/milestone/29)
- It should be possible and easy to use TechDocs in most environments across
organizations.
- Minimal bugs, better error handling and scalable backend and frontend.
- Documentation Search
- TechDocs Homepage with basic features
### **Future work 🔮**
**General Availability (GA) release** -
+8
View File
@@ -113,6 +113,14 @@ techdocs:
# https://docs.microsoft.com/en-us/azure/storage/common/storage-auth?toc=/azure/storage/blobs/toc.json
accountKey: ${TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY}
# (Optional and not recommended) Prior to version [0.x.y] of TechDocs, docs
# sites could only be accessed over paths with case-sensitive entity triplets
# e.g. (namespace/Kind/name). If you are upgrading from an older version of
# TechDocs and are unable to perform the necessary migration of files in your
# external storage, you can set this value to `true` to temporarily revert to
# the old, case-sensitive entity triplet behavior.
legacyUseCaseSensitiveTripletPaths: false
# (Optional and Legacy) TechDocs makes API calls to techdocs-backend using this URL. e.g. get docs of an entity, get metadata, etc.
# You don't have to specify this anymore.
+13 -3
View File
@@ -33,14 +33,24 @@ In `packages/app/src/App.tsx`, import `TechDocsPage` and add the following to
`FlatRoutes`:
```tsx
import { TechDocsPage } from '@backstage/plugin-techdocs';
import {
DefaultTechDocsHome,
TechDocsIndexPage,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
// ...
const AppRoutes = () => {
<FlatRoutes>
// ... other plugin routes
<Route path="/docs" element={<TechdocsPage />} />
<Route path="/docs" element={<TechDocsIndexPage />}>
<DefaultTechDocsHome />
</Route>
<Route
path="/docs/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
/>
</FlatRoutes>;
};
```
@@ -204,7 +214,7 @@ techdocs:
builder: 'local'
publisher:
type: 'local'
generators:
generator:
techdocs: local
```
+96 -69
View File
@@ -104,83 +104,110 @@ the repository. The archive does not have any git history attached to it. Also
it is a compressed file. Hence the file size is significantly smaller than how
much data git clone has to transfer.
## How to use a custom TechDocs home page?
## How to customize the TechDocs home page?
### 1st way: TechDocsCustomHome with a custom configuration
TechDocs uses a composability pattern similar to the Search and Catalog plugins
in Backstage. While a default table experience, similar to the one provided by
the Catalog plugin, is made available for ease-of-use, it's possible for you to
provide a completely custom experience, tailored to the needs of your
organization.
As an example, in your main App.tsx:
This is done in your `app` package. By default, you might see something like
this in your `App.tsx`:
```tsx
import {
TechDocsCustomHome,
PanelType,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
import { Entity } from '@backstage/catalog-model';
const tabsConfig = [
{
label: 'Custom Tab',
panels: [
{
title: 'Custom Documents Cards 1',
description:
'Explore your internal technical ecosystem through documentation.',
panelType: 'DocsCardGrid' as PanelType,
// optional, is applied to a container of the panel (excludes header of panel)
panelCSS: { maxHeight: '400px', overflow:'auto' },
filterPredicate: (entity: Entity) => !!entity.metadata.annotations?.['customCardAnnotationOne'];
},
{
title: 'Custom Documents Cards 2',
description:
'Explore your internal technical ecosystem through documentation.',
panelType: 'DocsCardGrid' as PanelType,
panelCSS: { maxHeight: '400px', overflow:'auto' },
filterPredicate: (entity: Entity) => !!entity.metadata.annotations?.['customCardAnnotationTwo'];
},
],
},
{
label: 'Overview',
panels: [
{
title: 'Overview',
description:
'Explore your internal technical ecosystem through documentation.',
panelType: 'DocsTable' as PanelType,
filterPredicate: () => true,
},
],
},
];
const routes = (
const AppRoutes = () => {
<FlatRoutes>
<Route
path="/docs"
element={<TechDocsCustomHome tabsConfig={tabsConfig} />}
/>
<Route
path="/docs/:namespace/:kind/:name/*"
element={<TechDocsReaderPage />}
/>
</FlatRoutes>
<Route path="/docs" element={<TechDocsIndexPage />}>
<DefaultTechDocsHome />
</Route>
</FlatRoutes>;
};
```
An example of tabsConfig that corresponds to the default documentation home page
can be found at `plugins/techdocs/src/home/components/TechDocsHome.tsx`.
But you can replace `<DefaultTechDocsHome />` with any React component, which
will be rendered in its place. Most likely, you would want to create and
maintain such a component in a new directory at
`packages/app/src/components/techdocs`, and import and use it in `App.tsx`:
Currently `panelType` has DocsCardGrid and DocsTable available. We currently
recommend that DocsCardGrid can be optionally vertically stacked by setting a
maxHeight using `panelCSS`, and DocsTable to be in a tab by itself.
```tsx
import { CustomTechDocsHome } from './components/techdocs/CustomTechDocsHome';
// ...
const AppRoutes = () => {
<FlatRoutes>
<Route path="/docs" element={<TechDocsIndexPage />}>
<CustomTechDocsHome />
</Route>
</FlatRoutes>;
};
```
### 2nd way: Custom home page plugin
## How to migrate from TechDocs Alpha to Beta
A custom home page plugin can be built that uses the components extensions
DocsCardGrid and DocsTable, exported from @backstage/techdocs. They both take a
array of documentation entities ( i.e.have a 'backstage.io/techdocs-ref'
annotation ) as an 'entities' attribute.
> This guide only applies to the "recommended" TechDocs deployment method (where
> an external storage provider and external CI/CD is used). If you use the
> "basic" or "out-of-the-box" setup, you can stop here! No action needed.
For a reference to the React structure of the default home page, please refer to
`plugins/techdocs/src/home/components/TechDocsCustomHome.tsx`.
The beta version of TechDocs (v0.x.y) made a breaking change to the way TechDocs
content was accessed and stored, allowing pages to be accessed with
case-insensitive entity triplet paths (e.g. `/docs/namespace/kind/name` whereas
in prior versions, they could only be accessed at `/docs/namespace/Kind/name`).
In order to enable this change, documentation has to be stored in an external
storage provider using an object key whose entity triplet is lower-cased.
New installations of TechDocs since the beta version will work fine with no
action, but for those who were running TechDocs prior to this version, a
migration will need to be performed so that all existing content in your storage
bucket matches this lower-case entity triplet expectation.
1. **Ensure you have the right permissions on your storage provider**: In order
to migrate files in your storage provider, the `techdocs-cli` needs to be
able to read/copy/rename/move/delete files. The exact instructions vary by
storage provider, but check the [using cloud storage][using-cloud-storage]
page for details.
2. **Run a non-destructive migration of files**: Ensure you have the latest
version of `techdocs-cli` installed. Then run the following command, using
the details relevant for your provider / configuration. This will copy all
files from, e.g. `namespace/Kind/name/index.html` to
`namespace/kind/name/index.html`, without removing the original files.
```sh
techdocs-cli migrate --publisher-type <awsS3|googleGcs|azureBlobStorage> --storage-name <bucket/container name> --verbose
```
3. **Deploy the updated versions of the TechDocs plugins**: Once the migration
above has been run, you can deploy the beta versions of the TechDocs backend
and frontend plugins to your Backstage instance.
4. **Verify that your TechDocs sites are still loading/accessible**: Try
accessing a TechDocs site using different entity-triplet case variants, e.g.
`/docs/namespace/KIND/name` or `/docs/namespace/kind/name`. Your TechDocs
site should load regardless of the URL path casing you use.
5. **Clean up the old objects from storage**: Once you've verified that your
TechDocs site is accessible, you can clean up your storage bucket by
re-running the `migrate` command on the TechDocs CLI, but with an additional
`removeOriginal` flag passed:
```sh
techdocs-cli migrate --publisher-type <awsS3|googleGcs|azureBlobStorage> --storage-name <bucket/container name> --removeOriginal --verbose
```
6. **Update your CI/CD pipelines to use the beta version of the TechDocs CLI**:
Finally, you can update all of your CI/CD pipelines to use at least v0.x.y of
the TechDocs CLI, ensuring that all sites are published to the new,
lower-cased entity triplet paths going forward.
If you encounter problems running this migration, please [report the
issue][beta-migrate-bug]. You can temporarily revert to pre-beta storage
expectations with a configuration change:
```yaml
techdocs:
legacyUseCaseSensitiveTripletPaths: true
```
[beta-migrate-bug]:
https://github.com/backstage/backstage/issues/new?assignees=&labels=bug&template=bug_template.md&title=[TechDocs]%20Unable%20to%20run%20beta%20migration
[using-cloud-storage]: ./using-cloud-storage.md
+62 -7
View File
@@ -399,9 +399,34 @@ techdocs:
Set the configs in your `app-config.yaml` to point to your container name.
https://docs.openstack.org/api-ref/identity/v3/?expanded=password-authentication-with-unscoped-authorization-detail#password-authentication-with-unscoped-authorization
https://docs.openstack.org/api-ref/identity/v3/?expanded=password-authentication-with-unscoped-authorization-detail,authenticating-with-an-application-credential-detail#authenticating-with-an-application-credential
for more details.
```yaml
techdocs:
publisher:
type: 'openStackSwift'
openStackSwift:
containerName: 'name-of-techdocs-storage-bucket'
credentials:
id: ${OPENSTACK_SWIFT_STORAGE_APPLICATION_CREDENTIALS_ID}
secret: ${OPENSTACK_SWIFT_STORAGE_APPLICATION_CREDENTIALS_SECRET}
authUrl: ${OPENSTACK_SWIFT_STORAGE_AUTH_URL}
swiftUrl: ${OPENSTACK_SWIFT_STORAGE_SWIFT_URL}
```
**4. That's it!**
Your Backstage app is now ready to use OpenStack Swift Storage for TechDocs, to
store and read the static generated documentation files. When you start the
backend of the app, you should be able to see
`techdocs info Successfully connected to the OpenStack Swift Storage container`
in the logs.
## Bonus: Migration from old OpenStack Swift Configuration
Let's assume we have the old OpenStack Swift configuration here.
```yaml
techdocs:
publisher:
@@ -418,10 +443,40 @@ techdocs:
region: ${OPENSTACK_SWIFT_STORAGE_REGION}
```
**4. That's it!**
##### Step 1: Change the credential keys
Your Backstage app is now ready to use OpenStack Swift Storage for TechDocs, to
store and read the static generated documentation files. When you start the
backend of the app, you should be able to see
`techdocs info Successfully connected to the OpenStack Swift Storage container`
in the logs.
Since the new SDK uses _Application Credentials_ to authenticate OpenStack, we
need to change the keys `credentials.username` to `credentials.id`,
`credentials.password` to `credentials.secret` and use Application Credential ID
and secret here. For more detail about credentials look
[here](https://docs.openstack.org/api-ref/identity/v3/?expanded=password-authentication-with-unscoped-authorization-detail,authenticating-with-an-application-credential-detail#authenticating-with-an-application-credential).
##### Step 2: Remove the unused keys
Since the new SDK doesn't use the old way authentication, we don't need the keys
`openStackSwift.keystoneAuthVersion`, `openStackSwift.domainId`,
`openStackSwift.domainName` and `openStackSwift.region`. So you can remove them.
##### Step 3: Add Swift URL
The new SDK needs the OpenStack Swift connection URL for connecting the Swift.
So you need to add a new key called `openStackSwift.swiftUrl` and give the
OpenStack Swift url here. Example url should look like that:
`https://example.com:6780/swift/v1`
##### That's it!
Your new configuration should look like that!
```yaml
techdocs:
publisher:
type: 'openStackSwift'
openStackSwift:
containerName: 'name-of-techdocs-storage-bucket'
credentials:
id: ${OPENSTACK_SWIFT_STORAGE_APPLICATION_CREDENTIALS_ID}
secret: ${OPENSTACK_SWIFT_STORAGE_APPLICATION_CREDENTIALS_SECRET}
authUrl: ${OPENSTACK_SWIFT_STORAGE_AUTH_URL}
swiftUrl: ${OPENSTACK_SWIFT_STORAGE_SWIFT_URL}
```
+42
View File
@@ -0,0 +1,42 @@
---
id: locations
sidebar_label: Locations
title: Amazon Web Services S3 Locations
# prettier-ignore
description: Setting up an integration with Amazon Web Services S3
---
The AWS S3 integration supports loading catalog entities from an S3 Bucket.
Entities can be added to
[static catalog configuration](../../features/software-catalog/configuration.md),
or registered with the
[catalog-import](https://github.com/backstage/backstage/tree/master/plugins/catalog-import)
plugin.
## Configuration
To use this integration, add configuration to your `app-config.yaml`:
```yaml
integrations:
awsS3:
- host: amazonaws.com
accessKeyId: ${AWS_ACCESS_KEY_ID}
secretAccessKey: ${AWS_SECRET_ACCESS_KEY}
```
Then make sure the environment variables `AWS_ACCESS_KEY_ID` and
`AWS_SECRET_ACCESS_KEY` are set when you run Backstage.
Users with multiple AWS accounts may want to use a role for S3 storage that is
in a different AWS account. Using the `roleArn` parameter as seen below, you can
instruct the AWS S3 reader to assume a role before accessing S3:
```yaml
integrations:
awsS3:
- host: amazonaws.com
accessKeyId: ${AWS_ACCESS_KEY_ID}
secretAccessKey: ${AWS_SECRET_ACCESS_KEY}
roleArn: 'arn:aws:iam::xxxxxxxxxxxx:role/example-role'
```
+36
View File
@@ -0,0 +1,36 @@
---
id: discovery
title: GitLab Discovery
sidebar_label: Discovery
# prettier-ignore
description: Automatically discovering catalog entities from repositories in GitLab
---
The GitLab integration has a special discovery processor for discovering catalog
entities from GitLab. The processor will crawl the GitLab instance and register
entities matching the configured path. This can be useful as an alternative to
static locations or manually adding things to the catalog.
To use the discovery processor, you'll need a GitLab integration
[set up](locations.md) with a `token`. Then you can add a location target to the
catalog configuration:
```yaml
catalog:
locations:
- type: gitlab-discovery
target: https://gitlab.com/group/subgroup/blob/main/catalog-info.yaml
```
Note the `gitlab-discovery` type, as this is not a regular `url` processor.
The target is composed of three parts:
- The base URL, `https://gitlab.com` in this case
- The group path, `group/subgroup` in this case. This is optional: If you omit
this path the processor will scan the entire GitLab instance instead.
- The path within each repository to find the catalog YAML file. This will
usually be `/blob/main/catalog-info.yaml`, `/blob/master/catalog-info.yaml` or
a similar variation for catalog files stored in the root directory of each
repository. If you want to use the repository's default branch use the `*`
wildcard, e.g.: `/blob/*/catalog-info.yaml`
@@ -0,0 +1,9 @@
---
title: AWS CloudFormation
author: Purple Technology
authorUrl: https://blog.purple-technology.com
category: Infrastructure
description: Load Backstage entities from AWS CloudFormation stacks
documentation: https://github.com/purple-technology/backstage-aws-cloudformation-plugin#readme
iconUrl: https://raw.githubusercontent.com/purple-technology/backstage-aws-cloudformation-plugin/master/docs/cloudformation-logo.png
npmPackageName: 'backstage-aws-cloudformation-plugin'
+9
View File
@@ -0,0 +1,9 @@
---
title: FireHydrant
author: FireHydrant
authorUrl: https://firehydrant.io/
category: Incident Management
description: View service incidents information from FireHydrant, such as active incidents and incident metrics, directly within Backstage.
documentation: https://github.com/backstage/backstage/blob/master/plugins/firehydrant/README.md
iconUrl: https://raw.githubusercontent.com/backstage/backstage/master/plugins/firehydrant/doc/firehydrant_logo.png
npmPackageName: '@backstage/plugin-firehydrant'
+12
View File
@@ -0,0 +1,12 @@
---
title: GitLab
author: Loblaw
authorUrl: https://github.com/loblaw-sre/backstage-plugin-gitlab
category: CI/CD
description: View GitLab pipelines, merge requests, languages and contributors.
documentation: https://github.com/loblaw-sre/backstage-plugin-gitlab
iconUrl: https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png
npmPackageName: '@loblaw/backstage-plugin-gitlab'
tags:
- ci
- cd
+5
View File
@@ -104,6 +104,11 @@
],
"Integrations": [
"integrations/index",
{
"type": "subcategory",
"label": "AWS S3",
"ids": ["integrations/aws-s3/locations"]
},
{
"type": "subcategory",
"label": "Azure",
+2
View File
@@ -79,6 +79,8 @@ nav:
- FAQ: 'features/techdocs/FAQ.md'
- Integrations:
- Overview: 'integrations/index.md'
- AWS S3:
- Locations: 'integrations/aws-s3/locations.md'
- Azure:
- Locations: 'integrations/azure/locations.md'
- Org Data: 'integrations/azure/org.md'
+32
View File
@@ -1,5 +1,37 @@
# example-app
## 0.2.43
### Patch Changes
- Updated dependencies
- @backstage/plugin-explore@0.3.14
- @backstage/core-app-api@0.1.10
- @backstage/plugin-catalog-import@0.5.19
- @backstage/plugin-catalog@0.6.13
- @backstage/plugin-org@0.3.20
- @backstage/plugin-catalog-react@0.4.4
- @backstage/core-components@0.3.3
- @backstage/plugin-techdocs@0.11.0
- @backstage/plugin-search@0.4.8
## 0.2.42
### Patch Changes
- Updated dependencies
- @backstage/plugin-scaffolder@0.11.1
- @backstage/plugin-home@0.2.0
- @backstage/cli@0.7.9
- @backstage/core-app-api@0.1.9
- @backstage/plugin-catalog-import@0.5.18
- @backstage/plugin-catalog@0.6.12
- @backstage/plugin-catalog-react@0.4.3
- @backstage/plugin-sentry@0.3.19
- @backstage/plugin-jenkins@0.5.4
- @backstage/core-components@0.3.2
- @backstage/theme@0.2.10
## 0.2.41
### Patch Changes
+20 -19
View File
@@ -1,54 +1,55 @@
{
"name": "example-app",
"version": "0.2.41",
"version": "0.2.43",
"private": true,
"bundled": true,
"dependencies": {
"@backstage/catalog-model": "^0.9.0",
"@backstage/cli": "^0.7.8",
"@backstage/core-app-api": "^0.1.8",
"@backstage/core-components": "^0.3.1",
"@backstage/cli": "^0.7.9",
"@backstage/core-app-api": "^0.1.10",
"@backstage/core-components": "^0.3.3",
"@backstage/core-plugin-api": "^0.1.6",
"@backstage/integration-react": "^0.1.7",
"@backstage/plugin-api-docs": "^0.6.6",
"@backstage/plugin-badges": "^0.2.7",
"@backstage/plugin-catalog": "^0.6.11",
"@backstage/plugin-catalog-import": "^0.5.17",
"@backstage/plugin-catalog-react": "^0.4.2",
"@backstage/plugin-catalog": "^0.6.13",
"@backstage/plugin-catalog-import": "^0.5.19",
"@backstage/plugin-catalog-react": "^0.4.4",
"@backstage/plugin-circleci": "^0.2.21",
"@backstage/plugin-cloudbuild": "^0.2.21",
"@backstage/plugin-code-coverage": "^0.1.9",
"@backstage/plugin-cost-insights": "^0.11.4",
"@backstage/plugin-explore": "^0.3.13",
"@backstage/plugin-explore": "^0.3.14",
"@backstage/plugin-gcp-projects": "^0.3.2",
"@backstage/plugin-github-actions": "^0.4.16",
"@backstage/plugin-graphiql": "^0.2.14",
"@backstage/plugin-jenkins": "^0.5.3",
"@backstage/plugin-home": "^0.2.0",
"@backstage/plugin-jenkins": "^0.5.4",
"@backstage/plugin-kafka": "^0.2.13",
"@backstage/plugin-kubernetes": "^0.4.11",
"@backstage/plugin-lighthouse": "^0.2.23",
"@backstage/plugin-newrelic": "^0.3.2",
"@backstage/plugin-org": "^0.3.19",
"@backstage/plugin-org": "^0.3.20",
"@backstage/plugin-pagerduty": "0.3.11",
"@backstage/plugin-rollbar": "^0.3.12",
"@backstage/plugin-scaffolder": "^0.11.0",
"@backstage/plugin-search": "^0.4.7",
"@backstage/plugin-sentry": "^0.3.18",
"@backstage/plugin-scaffolder": "^0.11.1",
"@backstage/plugin-search": "^0.4.8",
"@backstage/plugin-sentry": "^0.3.19",
"@backstage/plugin-shortcuts": "^0.1.6",
"@backstage/plugin-tech-radar": "^0.4.5",
"@backstage/plugin-techdocs": "^0.10.4",
"@backstage/plugin-techdocs": "^0.11.0",
"@backstage/plugin-todo": "^0.1.7",
"@backstage/plugin-user-settings": "^0.3.3",
"@backstage/search-common": "^0.1.3",
"@backstage/theme": "^0.2.9",
"@backstage/theme": "^0.2.10",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
"@octokit/rest": "^18.5.3",
"@roadiehq/backstage-plugin-buildkite": "^1.0.6",
"@roadiehq/backstage-plugin-github-insights": "^1.1.20",
"@roadiehq/backstage-plugin-github-pull-requests": "^1.0.10",
"@roadiehq/backstage-plugin-travis-ci": "^1.0.8",
"@roadiehq/backstage-plugin-buildkite": "^1.0.8",
"@roadiehq/backstage-plugin-github-insights": "^1.1.23",
"@roadiehq/backstage-plugin-github-pull-requests": "^1.0.13",
"@roadiehq/backstage-plugin-travis-ci": "^1.0.11",
"history": "^5.0.0",
"prop-types": "^15.7.2",
"react": "^16.12.0",
+9 -8
View File
@@ -20,6 +20,7 @@ import {
OAuthRequestDialog,
SignInPage,
} from '@backstage/core-components';
import { HomepageCompositionRoot } from '@backstage/plugin-home';
import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs';
import {
CatalogEntityPage,
@@ -45,16 +46,13 @@ import {
ScaffolderPage,
scaffolderPlugin,
ScaffolderFieldExtensions,
RepoUrlPickerFieldExtension,
OwnerPickerFieldExtension,
EntityPickerFieldExtension,
EntityNamePickerFieldExtension,
} from '@backstage/plugin-scaffolder';
import { SearchPage } from '@backstage/plugin-search';
import { TechRadarPage } from '@backstage/plugin-tech-radar';
import {
DefaultTechDocsHome,
TechDocsIndexPage,
techdocsPlugin,
TechDocsReaderPage,
} from '@backstage/plugin-techdocs';
import { UserSettingsPage } from '@backstage/plugin-user-settings';
@@ -67,6 +65,8 @@ import { Root } from './components/Root';
import { entityPage } from './components/catalog/EntityPage';
import { searchPage } from './components/search/SearchPage';
import { LowerCaseValuePickerFieldExtension } from './components/scaffolder/customScaffolderExtensions';
import { HomePage } from './components/home/HomePage';
import { providers } from './identityProviders';
import * as plugins from './plugins';
@@ -93,6 +93,7 @@ const app = createApp({
bindRoutes({ bind }) {
bind(catalogPlugin.externalRoutes, {
createComponent: scaffolderPlugin.routes.root,
viewTechDoc: techdocsPlugin.routes.docRoot,
});
bind(apiDocsPlugin.externalRoutes, {
createComponent: scaffolderPlugin.routes.root,
@@ -112,6 +113,10 @@ const AppRouter = app.getRouter();
const routes = (
<FlatRoutes>
<Navigate key="/" to="catalog" />
{/* TODO(rubenl): Move this to / once its more mature and components exist */}
<Route path="/home" element={<HomepageCompositionRoot />}>
<HomePage />
</Route>
<Route path="/catalog" element={<CatalogIndexPage />} />
<Route
path="/catalog/:namespace/:kind/:name"
@@ -129,10 +134,6 @@ const routes = (
/>
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderFieldExtensions>
<EntityPickerFieldExtension />
<EntityNamePickerFieldExtension />
<RepoUrlPickerFieldExtension />
<OwnerPickerFieldExtension />
<LowerCaseValuePickerFieldExtension />
</ScaffolderFieldExtensions>
</Route>
@@ -211,20 +211,8 @@ const cicdCard = (
</EntitySwitch>
);
const errorsContent = (
<EntitySwitch>
<EntitySwitch.Case if={isRollbarAvailable}>
<EntityRollbarContent />
</EntitySwitch.Case>
<EntitySwitch.Case>
<EntitySentryContent />
</EntitySwitch.Case>
</EntitySwitch>
);
const overviewContent = (
<Grid container spacing={3} alignItems="stretch">
const entityWarningContent = (
<>
<EntitySwitch>
<EntitySwitch.Case if={isOrphan}>
<Grid item xs={12}>
@@ -240,7 +228,24 @@ const overviewContent = (
</Grid>
</EntitySwitch.Case>
</EntitySwitch>
</>
);
const errorsContent = (
<EntitySwitch>
<EntitySwitch.Case if={isRollbarAvailable}>
<EntityRollbarContent />
</EntitySwitch.Case>
<EntitySwitch.Case>
<EntitySentryContent />
</EntitySwitch.Case>
</EntitySwitch>
);
const overviewContent = (
<Grid container spacing={3} alignItems="stretch">
{entityWarningContent}
<Grid item md={8} xs={12}>
<EntityAboutCard variant="gridItem" />
</Grid>
@@ -448,6 +453,7 @@ const apiPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
{entityWarningContent}
<Grid item xs={12}>
<EntityAboutCard />
</Grid>
@@ -478,6 +484,7 @@ const userPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
{entityWarningContent}
<Grid item xs={12} md={6}>
<EntityUserProfileCard variant="gridItem" />
</Grid>
@@ -493,6 +500,7 @@ const groupPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3}>
{entityWarningContent}
<Grid item xs={12} md={6}>
<EntityGroupProfileCard variant="gridItem" />
</Grid>
@@ -511,6 +519,7 @@ const systemPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3} alignItems="stretch">
{entityWarningContent}
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
@@ -535,6 +544,7 @@ const domainPage = (
<EntityLayoutWrapper>
<EntityLayout.Route path="/" title="Overview">
<Grid container spacing={3} alignItems="stretch">
{entityWarningContent}
<Grid item md={6}>
<EntityAboutCard variant="gridItem" />
</Grid>
@@ -0,0 +1,72 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import Grid from '@material-ui/core/Grid';
import {
RandomJokeHomePageComponent,
ComponentAccordion,
ComponentTabs,
ComponentTab,
} from '@backstage/plugin-home';
export const HomePage = () => (
<Grid container spacing={3}>
<Grid item xs={12} md={4}>
<RandomJokeHomePageComponent />
</Grid>
<Grid item xs={12} md={4}>
<RandomJokeHomePageComponent
defaultCategory="any"
Renderer={ComponentAccordion}
/>
<RandomJokeHomePageComponent
title="Another Random Joke"
Renderer={ComponentAccordion}
/>
<RandomJokeHomePageComponent
title="One More Random Joke"
defaultCategory="programming"
Renderer={ComponentAccordion}
/>
</Grid>
<Grid item xs={12} md={4}>
<ComponentTabs
title="Random Jokes"
tabs={[
{
label: 'Programming',
Component: () => (
<RandomJokeHomePageComponent
defaultCategory="programming"
Renderer={ComponentTab}
/>
),
},
{
label: 'Any',
Component: () => (
<RandomJokeHomePageComponent
defaultCategory="any"
Renderer={ComponentTab}
/>
),
},
]}
/>
</Grid>
</Grid>
);
+25
View File
@@ -1,5 +1,30 @@
# @backstage/backend-common
## 0.9.0
### Minor Changes
- a365f1faf: The `ZipArchiveResponse` class now accepts an optional `stripFirstDirectory` parameter. Note that its default value is `false`, which leads to a breaking change in behaviour to previous versions of the class. If you use this class explicitly and want to retain the old behaviour, add a `true` parameter value to its constructor.
### Patch Changes
- Updated dependencies
- @backstage/integration@0.6.2
- @backstage/config@0.1.8
## 0.8.10
### Patch Changes
- 8543d9890: Add an optional `info` parameter to the `readTree` filter option with a `size` property.
- 4d909268c: Read responses in `UrlReader#read()` as array buffer instead of as text to allow reading non-text locations such as images.
- 9b4604b38: Add support for watching configuration by implementing the `subscribe` method in the configuration returned by `loadBackendConfig`.
- b8cb12009: Add AWS S3 URL Reader
- Updated dependencies
- @backstage/config@0.1.7
- @backstage/config-loader@0.6.7
- @backstage/integration@0.6.1
## 0.8.9
### Patch Changes
+65 -11
View File
@@ -42,23 +42,14 @@ export class AzureUrlReader implements UrlReader {
treeResponseFactory: ReadTreeResponseFactory;
},
);
// Warning: (ae-forgotten-export) The symbol "ReaderFactory" needs to be exported by the entry point index.d.ts
//
// (undocumented)
static factory: ReaderFactory;
// (undocumented)
read(url: string): Promise<Buffer>;
// Warning: (ae-forgotten-export) The symbol "ReadTreeOptions" needs to be exported by the entry point index.d.ts
//
// (undocumented)
readTree(url: string, options?: ReadTreeOptions): Promise<ReadTreeResponse>;
// Warning: (ae-forgotten-export) The symbol "ReadUrlOptions" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "ReadUrlResponse" needs to be exported by the entry point index.d.ts
//
// (undocumented)
readUrl(url: string, _options?: ReadUrlOptions): Promise<ReadUrlResponse>;
// Warning: (ae-forgotten-export) The symbol "SearchOptions" needs to be exported by the entry point index.d.ts
//
// (undocumented)
search(url: string, options?: SearchOptions): Promise<SearchResponse>;
// (undocumented)
@@ -415,6 +406,28 @@ export type PluginEndpointDiscovery = {
getExternalBaseUrl(pluginId: string): Promise<string>;
};
// Warning: (ae-missing-release-tag) "ReaderFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type ReaderFactory = (options: {
config: Config;
logger: Logger_2;
treeResponseFactory: ReadTreeResponseFactory;
}) => UrlReaderPredicateTuple[];
// Warning: (ae-missing-release-tag) "ReadTreeOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type ReadTreeOptions = {
filter?(
path: string,
info?: {
size: number;
},
): boolean;
etag?: string;
};
// Warning: (ae-missing-release-tag) "ReadTreeResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
@@ -425,6 +438,18 @@ export type ReadTreeResponse = {
etag: string;
};
// Warning: (ae-missing-release-tag) "ReadTreeResponseFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export interface ReadTreeResponseFactory {
// Warning: (ae-forgotten-export) The symbol "FromArchiveOptions" needs to be exported by the entry point index.d.ts
//
// (undocumented)
fromTarArchive(options: FromArchiveOptions): Promise<ReadTreeResponse>;
// (undocumented)
fromZipArchive(options: FromArchiveOptions): Promise<ReadTreeResponse>;
}
// Warning: (ae-missing-release-tag) "ReadTreeResponseFile" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
@@ -433,6 +458,21 @@ export type ReadTreeResponseFile = {
content(): Promise<Buffer>;
};
// Warning: (ae-missing-release-tag) "ReadUrlOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type ReadUrlOptions = {
etag?: string;
};
// Warning: (ae-missing-release-tag) "ReadUrlResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type ReadUrlResponse = {
buffer(): Promise<Buffer>;
etag?: string;
};
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// Warning: (ae-missing-release-tag) "requestLoggingHandler" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -472,6 +512,13 @@ export type RunContainerOptions = {
pullImage?: boolean;
};
// Warning: (ae-missing-release-tag) "SearchOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export type SearchOptions = {
etag?: string;
};
// Warning: (ae-missing-release-tag) "SearchResponse" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
@@ -561,6 +608,14 @@ export type UrlReader = {
search(url: string, options?: SearchOptions): Promise<SearchResponse>;
};
// Warning: (ae-missing-release-tag) "UrlReaderPredicateTuple" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type UrlReaderPredicateTuple = {
predicate: (url: URL) => boolean;
reader: UrlReader;
};
// Warning: (ae-missing-release-tag) "UrlReaders" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
@@ -592,8 +647,7 @@ export function useHotMemoize<T>(_module: NodeModule, valueFactory: () => T): T;
//
// src/cache/types.d.ts:34:5 - (ae-forgotten-export) The symbol "ClientOptions" needs to be exported by the entry point index.d.ts
// src/middleware/errorHandler.d.ts:17:26 - (tsdoc-malformed-html-name) Invalid HTML element: A space is not allowed here
// src/reading/AzureUrlReader.d.ts:9:9 - (ae-forgotten-export) The symbol "ReadTreeResponseFactory" needs to be exported by the entry point index.d.ts
// src/reading/types.d.ts:106:5 - (ae-forgotten-export) The symbol "ReadTreeResponseDirOptions" needs to be exported by the entry point index.d.ts
// src/reading/types.d.ts:108:5 - (ae-forgotten-export) The symbol "ReadTreeResponseDirOptions" needs to be exported by the entry point index.d.ts
// src/service/types.d.ts:12:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/service/types.d.ts:22:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
// src/service/types.d.ts:30:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
+7 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
"version": "0.8.9",
"version": "0.9.0",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -30,16 +30,17 @@
},
"dependencies": {
"@backstage/cli-common": "^0.1.2",
"@backstage/config": "^0.1.6",
"@backstage/config-loader": "^0.6.6",
"@backstage/config": "^0.1.8",
"@backstage/config-loader": "^0.6.7",
"@backstage/errors": "^0.1.1",
"@backstage/integration": "^0.6.0",
"@backstage/integration": "^0.6.2",
"@google-cloud/storage": "^5.8.0",
"@octokit/rest": "^18.5.3",
"@types/cors": "^2.8.6",
"@types/dockerode": "^3.2.1",
"@types/express": "^4.17.6",
"archiver": "^5.0.2",
"aws-sdk": "^2.840.0",
"compression": "^1.7.4",
"concat-stream": "^2.0.0",
"cors": "^2.8.5",
@@ -76,7 +77,7 @@
}
},
"devDependencies": {
"@backstage/cli": "^0.7.8",
"@backstage/cli": "^0.7.9",
"@backstage/test-utils": "^0.1.17",
"@types/archiver": "^5.1.0",
"@types/compression": "^1.7.0",
@@ -92,6 +93,7 @@
"@types/tar": "^4.0.3",
"@types/unzipper": "^0.10.3",
"@types/webpack-env": "^1.15.2",
"aws-sdk-mock": "^5.2.1",
"get-port": "^5.1.1",
"http-errors": "^1.7.3",
"jest": "^26.0.1",
+115 -5
View File
@@ -18,32 +18,142 @@ import { resolve as resolvePath } from 'path';
import parseArgs from 'minimist';
import { Logger } from 'winston';
import { findPaths } from '@backstage/cli-common';
import { Config, ConfigReader } from '@backstage/config';
import { Config, ConfigReader, JsonValue } from '@backstage/config';
import { loadConfig } from '@backstage/config-loader';
class ObservableConfigProxy implements Config {
private config: Config = new ConfigReader({});
private readonly subscribers: (() => void)[] = [];
constructor(private readonly logger: Logger) {}
setConfig(config: Config) {
this.config = config;
for (const subscriber of this.subscribers) {
try {
subscriber();
} catch (error) {
this.logger.error(`Config subscriber threw error, ${error}`);
}
}
}
subscribe(onChange: () => void): { unsubscribe: () => void } {
this.subscribers.push(onChange);
return {
unsubscribe: () => {
const index = this.subscribers.indexOf(onChange);
if (index >= 0) {
this.subscribers.splice(index, 1);
}
},
};
}
has(key: string): boolean {
return this.config.has(key);
}
keys(): string[] {
return this.config.keys();
}
get<T = JsonValue>(key?: string): T {
return this.config.get(key);
}
getOptional<T = JsonValue>(key?: string): T | undefined {
return this.config.getOptional(key);
}
getConfig(key: string): Config {
return this.config.getConfig(key);
}
getOptionalConfig(key: string): Config | undefined {
return this.config.getOptionalConfig(key);
}
getConfigArray(key: string): Config[] {
return this.config.getConfigArray(key);
}
getOptionalConfigArray(key: string): Config[] | undefined {
return this.config.getOptionalConfigArray(key);
}
getNumber(key: string): number {
return this.config.getNumber(key);
}
getOptionalNumber(key: string): number | undefined {
return this.config.getOptionalNumber(key);
}
getBoolean(key: string): boolean {
return this.config.getBoolean(key);
}
getOptionalBoolean(key: string): boolean | undefined {
return this.config.getOptionalBoolean(key);
}
getString(key: string): string {
return this.config.getString(key);
}
getOptionalString(key: string): string | undefined {
return this.config.getOptionalString(key);
}
getStringArray(key: string): string[] {
return this.config.getStringArray(key);
}
getOptionalStringArray(key: string): string[] | undefined {
return this.config.getOptionalStringArray(key);
}
}
type Options = {
logger: Logger;
// process.argv or any other overrides
argv: string[];
};
// A global used to ensure that only a single file watcher is active at a time.
let currentCancelFunc: () => void;
/**
* Load configuration for a Backend
* Load configuration for a Backend.
*
* This function should only be called once, during the initialization of the backend.
*/
export async function loadBackendConfig(options: Options): Promise<Config> {
const args = parseArgs(options.argv);
const configOpts: string[] = [args.config ?? []].flat();
const configPaths: string[] = [args.config ?? []].flat();
const config = new ObservableConfigProxy(options.logger);
/* eslint-disable-next-line no-restricted-syntax */
const paths = findPaths(__dirname);
const configs = await loadConfig({
configRoot: paths.targetRoot,
configPaths: configOpts.map(opt => resolvePath(opt)),
configPaths: configPaths.map(opt => resolvePath(opt)),
watch: {
onChange(newConfigs) {
options.logger.info(
`Reloaded config from ${newConfigs.map(c => c.context).join(', ')}`,
);
config.setConfig(ConfigReader.fromConfigs(newConfigs));
},
stopSignal: new Promise(resolve => {
if (currentCancelFunc) {
currentCancelFunc();
}
currentCancelFunc = resolve;
// For reloads of this module we need to use a dispose handler rather than the global.
if (module.hot) {
module.hot.addDisposeHandler(resolve);
}
}),
},
});
options.logger.info(
`Loaded config from ${configs.map(c => c.context).join(', ')}`,
);
return ConfigReader.fromConfigs(configs);
config.setConfig(ConfigReader.fromConfigs(configs));
return config;
}
@@ -0,0 +1,232 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ConfigReader, JsonObject } from '@backstage/config';
import { getVoidLogger } from '../logging';
import { DefaultReadTreeResponseFactory } from './tree';
import { AwsS3UrlReader } from './AwsS3UrlReader';
import {
AwsS3Integration,
readAwsS3IntegrationConfig,
} from '@backstage/integration';
import { UrlReaderPredicateTuple } from './types';
import AWSMock from 'aws-sdk-mock';
import aws from 'aws-sdk';
import path from 'path';
describe('AwsS3UrlReader', () => {
const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => {
return AwsS3UrlReader.factory({
config: new ConfigReader(config),
logger: getVoidLogger(),
treeResponseFactory: DefaultReadTreeResponseFactory.create({
config: new ConfigReader({}),
}),
});
};
afterEach(() => {
AWSMock.restore();
});
it('creates a dummy reader without the awsS3 field', () => {
const entries = createReader({
integrations: {},
});
expect(entries).toHaveLength(1);
});
it('creates a reader with credentials correctly configured', () => {
const awsS3Integrations = [];
awsS3Integrations.push({
host: 'amazonaws.com',
accessKeyId: 'fakekey',
secretAccessKey: 'fakekey',
});
const entries = createReader({
integrations: {
awsS3: awsS3Integrations,
},
});
expect(entries).toHaveLength(1);
});
it('creates a reader with default credentials provider', () => {
const awsS3Integrations = [];
awsS3Integrations.push({
host: 'amazonaws.com',
});
const entries = createReader({
integrations: {
awsS3: awsS3Integrations,
},
});
expect(entries).toHaveLength(1);
});
describe('predicates', () => {
const readers = createReader({
integrations: {
awsS3: [{}],
},
});
const predicate = readers[0].predicate;
it('returns true for the correct aws s3 storage host', () => {
expect(
predicate(new URL('https://test-bucket.s3.us-east-2.amazonaws.com')),
).toBe(true);
});
it('returns true for a url with the full path and the correct host', () => {
expect(
predicate(
new URL(
'https://test-bucket.s3.us-east-2.amazonaws.com/team/service/catalog-info.yaml',
),
),
).toBe(true);
});
it('returns false for an incorrect host', () => {
expect(predicate(new URL('https://amazon.com'))).toBe(false);
});
it('returns false for a completely different host', () => {
expect(predicate(new URL('https://storage.cloud.google.com'))).toBe(
false,
);
});
it("returns true for a url with a bucket with '.'", () => {
expect(
predicate(
new URL(
'https://test.bucket.s3.us-east-2.amazonaws.com/team/service/catalog-info.yaml',
),
),
).toBe(true);
});
});
describe('read', () => {
AWSMock.setSDKInstance(aws);
AWSMock.mock(
'S3',
'getObject',
Buffer.from(
require('fs').readFileSync(
path.resolve(
'src',
'reading',
'__fixtures__',
'awsS3-mock-object.yaml',
),
),
),
);
const s3 = new aws.S3();
const awsS3UrlReader = new AwsS3UrlReader(
new AwsS3Integration(
readAwsS3IntegrationConfig(
new ConfigReader({
host: 'amazonaws.com',
accessKeyId: 'fake-access-key',
secretAccessKey: 'fake-secret-key',
}),
),
),
s3,
);
it('returns contents of an object in a bucket', async () => {
const response = await awsS3UrlReader.read(
'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml',
);
expect(response.toString().trim()).toBe('site_name: Test');
});
it('rejects unknown targets', async () => {
await expect(
awsS3UrlReader.read(
'https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml',
),
).rejects.toThrow(
Error(
`Could not retrieve file from S3: not a valid AWS S3 URL: https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml`,
),
);
});
});
describe('readUrl', () => {
AWSMock.setSDKInstance(aws);
AWSMock.mock(
'S3',
'getObject',
Buffer.from(
require('fs').readFileSync(
path.resolve(
'src',
'reading',
'__fixtures__',
'awsS3-mock-object.yaml',
),
),
),
);
const s3 = new aws.S3();
const awsS3UrlReader = new AwsS3UrlReader(
new AwsS3Integration(
readAwsS3IntegrationConfig(
new ConfigReader({
host: 'amazonaws.com',
accessKeyId: 'fake-access-key',
secretAccessKey: 'fake-secret-key',
}),
),
),
s3,
);
it('returns contents of an object in a bucket', async () => {
const response = await awsS3UrlReader.readUrl(
'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml',
);
const buffer = await response.buffer();
expect(buffer.toString().trim()).toBe('site_name: Test');
});
it('rejects unknown targets', async () => {
await expect(
awsS3UrlReader.readUrl(
'https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml',
),
).rejects.toThrow(
Error(
`Could not retrieve file from S3: not a valid AWS S3 URL: https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml`,
),
);
});
});
});
@@ -0,0 +1,175 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import aws, { Credentials, S3 } from 'aws-sdk';
import { CredentialsOptions } from 'aws-sdk/lib/credentials';
import {
ReaderFactory,
ReadTreeResponse,
ReadUrlOptions,
ReadUrlResponse,
SearchResponse,
UrlReader,
} from './types';
import getRawBody from 'raw-body';
import { AwsS3Integration, ScmIntegrations } from '@backstage/integration';
const parseURL = (
url: string,
): { path: string; bucket: string; region: string } => {
let { host, pathname } = new URL(url);
/**
* Removes the leading '/' from the pathname to be processed
* as a parameter by AWS S3 SDK getObject method.
*/
pathname = pathname.substr(1);
/**
* Checks that the given URL is a valid S3 object url.
* Format of a Valid S3 URL: https://bucket-name.s3.Region.amazonaws.com/keyname
*/
const validHost = new RegExp(
/^[a-z\d][a-z\d\.-]{1,61}[a-z\d]\.s3\.[a-z\d-]+\.amazonaws.com$/,
);
if (!validHost.test(host)) {
throw new Error(`not a valid AWS S3 URL: ${url}`);
}
const [bucket] = host.split(/\.s3\.[a-z\d-]+\.amazonaws.com/);
host = host.substring(bucket.length);
const [, , region, ,] = host.split('.');
return {
path: pathname,
bucket: bucket,
region: region,
};
};
export class AwsS3UrlReader implements UrlReader {
static factory: ReaderFactory = ({ config }) => {
const integrations = ScmIntegrations.fromConfig(config);
return integrations.awsS3.list().map(integration => {
const creds = AwsS3UrlReader.buildCredentials(integration);
const s3 = new S3({
apiVersion: '2006-03-01',
credentials: creds,
});
const reader = new AwsS3UrlReader(integration, s3);
const predicate = (url: URL) =>
url.host.endsWith(integration.config.host);
return { reader, predicate };
});
};
constructor(
private readonly integration: AwsS3Integration,
private readonly s3: S3,
) {}
/**
* If accesKeyId and secretAccessKey are missing, the standard credentials provider chain will be used:
* https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html
*/
private static buildCredentials(
integration?: AwsS3Integration,
): Credentials | CredentialsOptions | undefined {
if (!integration) {
return undefined;
}
const accessKeyId = integration.config.accessKeyId;
const secretAccessKey = integration.config.secretAccessKey;
let explicitCredentials: Credentials | undefined;
if (accessKeyId && secretAccessKey) {
explicitCredentials = new Credentials({
accessKeyId,
secretAccessKey,
});
}
const roleArn = integration.config.roleArn;
if (roleArn) {
return new aws.ChainableTemporaryCredentials({
masterCredentials: explicitCredentials,
params: {
RoleSessionName: 'backstage-aws-s3-url-reader',
RoleArn: roleArn,
},
});
}
return explicitCredentials;
}
async read(url: string): Promise<Buffer> {
const response = await this.readUrl(url);
return response.buffer();
}
async readUrl(
url: string,
options?: ReadUrlOptions,
): Promise<ReadUrlResponse> {
try {
const { path, bucket, region } = parseURL(url);
aws.config.update({ region: region });
let params;
if (options?.etag) {
params = {
Bucket: bucket,
Key: path,
IfNoneMatch: options.etag,
};
} else {
params = {
Bucket: bucket,
Key: path,
};
}
const response = this.s3.getObject(params);
const buffer = await getRawBody(response.createReadStream());
const etag = (await response.promise()).ETag;
return {
buffer: async () => buffer,
etag: etag,
};
} catch (e) {
throw new Error(`Could not retrieve file from S3: ${e.message}`);
}
}
async readTree(): Promise<ReadTreeResponse> {
throw new Error('AwsS3Reader does not implement readTree');
}
async search(): Promise<SearchResponse> {
throw new Error('AwsS3Reader does not implement search');
}
toString() {
const secretAccessKey = this.integration.config.secretAccessKey;
return `awsS3{host=${this.integration.config.host},authed=${Boolean(
secretAccessKey,
)}}`;
}
}
@@ -70,7 +70,7 @@ export class AzureUrlReader implements UrlReader {
// for private repos when PAT is not valid, Azure API returns a http status code 203 with sign in page html
if (response.ok && response.status !== 203) {
return Buffer.from(await response.text());
return Buffer.from(await response.arrayBuffer());
}
const message = `${url} could not be read as ${builtUrl}, ${response.status} ${response.statusText}`;
@@ -86,7 +86,7 @@ export class BitbucketUrlReader implements UrlReader {
}
if (response.ok) {
return Buffer.from(await response.text());
return Buffer.from(await response.arrayBuffer());
}
const message = `${url} could not be read as ${bitbucketUrl}, ${response.status} ${response.statusText}`;
@@ -82,7 +82,7 @@ export class FetchUrlReader implements UrlReader {
if (response.ok) {
return {
buffer: async () => Buffer.from(await response.text()),
buffer: async () => Buffer.from(await response.arrayBuffer()),
etag: response.headers.get('ETag') ?? undefined,
};
}
@@ -119,7 +119,7 @@ export class GithubUrlReader implements UrlReader {
if (response.ok) {
return {
buffer: async () => Buffer.from(await response.text()),
buffer: async () => Buffer.from(await response.arrayBuffer()),
etag: response.headers.get('ETag') ?? undefined,
};
}
@@ -84,7 +84,7 @@ export class GitlabUrlReader implements UrlReader {
if (response.ok) {
return {
buffer: async () => Buffer.from(await response.text()),
buffer: async () => Buffer.from(await response.arrayBuffer()),
etag: response.headers.get('ETag') ?? undefined,
};
}
@@ -25,6 +25,7 @@ import { GitlabUrlReader } from './GitlabUrlReader';
import { DefaultReadTreeResponseFactory } from './tree';
import { FetchUrlReader } from './FetchUrlReader';
import { GoogleGcsUrlReader } from './GoogleGcsUrlReader';
import { AwsS3UrlReader } from './AwsS3UrlReader';
type CreateOptions = {
/** Root config object */
@@ -74,6 +75,7 @@ export class UrlReaders {
GithubUrlReader.factory,
GitlabUrlReader.factory,
GoogleGcsUrlReader.factory,
AwsS3UrlReader.factory,
FetchUrlReader.factory,
]),
});
@@ -0,0 +1 @@
site_name: Test
@@ -19,10 +19,17 @@ export { BitbucketUrlReader } from './BitbucketUrlReader';
export { GithubUrlReader } from './GithubUrlReader';
export { GitlabUrlReader } from './GitlabUrlReader';
export type {
ReaderFactory,
ReadTreeOptions,
ReadTreeResponse,
ReadTreeResponseFactory,
ReadTreeResponseFile,
ReadUrlOptions,
ReadUrlResponse,
SearchOptions,
SearchResponse,
SearchResponseFile,
UrlReader,
UrlReaderPredicateTuple,
} from './types';
export { UrlReaders } from './UrlReaders';
@@ -43,7 +43,7 @@ export class TarArchiveResponse implements ReadTreeResponse {
private readonly subPath: string,
private readonly workDir: string,
public readonly etag: string,
private readonly filter?: (path: string) => boolean,
private readonly filter?: (path: string, info: { size: number }) => boolean,
) {
if (subPath) {
if (!subPath.endsWith('/')) {
@@ -92,7 +92,7 @@ export class TarArchiveResponse implements ReadTreeResponse {
const path = relativePath.slice(this.subPath.length);
if (this.filter) {
if (!this.filter(path)) {
if (!this.filter(path, { size: entry.remain })) {
entry.resume();
return;
}
@@ -155,7 +155,7 @@ export class TarArchiveResponse implements ReadTreeResponse {
tar.extract({
strip,
cwd: dir,
filter: path => {
filter: (path, stat) => {
// 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 = stripFirstDirectoryFromPath(path);
@@ -164,7 +164,7 @@ export class TarArchiveResponse implements ReadTreeResponse {
}
if (this.filter) {
const innerPath = path.split('/').slice(strip).join('/');
return this.filter(innerPath);
return this.filter(innerPath, { size: stat.size });
}
return true;
},
@@ -22,11 +22,15 @@ import { ZipArchiveResponse } from './ZipArchiveResponse';
const archiveData = fs.readFileSync(
resolvePath(__filename, '../../__fixtures__/mock-main.zip'),
);
const archiveDataWithExtraDir = fs.readFileSync(
resolvePath(__filename, '../../__fixtures__/mock-with-extra-root-dir.zip'),
);
describe('ZipArchiveResponse', () => {
beforeEach(() => {
mockFs({
'/test-archive.zip': archiveData,
'/test-archive-with-extra-root-dir.zip': archiveDataWithExtraDir,
'/tmp': mockFs.directory(),
});
});
@@ -58,6 +62,36 @@ describe('ZipArchiveResponse', () => {
]);
});
it('should read files and strip root dir if requested', async () => {
const stream = fs.createReadStream('/test-archive-with-extra-root-dir.zip');
const res = new ZipArchiveResponse(
stream,
'',
'/tmp',
'etag',
undefined,
true,
);
const files = await res.files();
expect(files).toEqual([
{
path: 'mkdocs.yml',
content: expect.any(Function),
},
{
path: 'docs/index.md',
content: expect.any(Function),
},
]);
const contents = await Promise.all(files.map(f => f.content()));
expect(contents.map(c => c.toString('utf8').trim())).toEqual([
'site_name: Test',
'# Test',
]);
});
it('should read files with filter', async () => {
const stream = fs.createReadStream('/test-archive.zip');
@@ -37,7 +37,8 @@ export class ZipArchiveResponse implements ReadTreeResponse {
private readonly subPath: string,
private readonly workDir: string,
public readonly etag: string,
private readonly filter?: (path: string) => boolean,
private readonly filter?: (path: string, info: { size: number }) => boolean,
private readonly stripFirstDirectory?: boolean,
) {
if (subPath) {
if (!subPath.endsWith('/')) {
@@ -67,7 +68,9 @@ export class ZipArchiveResponse implements ReadTreeResponse {
}
private shouldBeIncluded(entry: Entry): boolean {
const strippedPath = stripFirstDirectoryFromPath(entry.path);
const strippedPath = this.stripFirstDirectory
? stripFirstDirectoryFromPath(entry.path)
: entry.path;
if (this.subPath) {
if (!strippedPath.startsWith(this.subPath)) {
@@ -75,7 +78,11 @@ export class ZipArchiveResponse implements ReadTreeResponse {
}
}
if (this.filter) {
return this.filter(this.getInnerPath(entry.path));
return this.filter(this.getInnerPath(entry.path), {
size:
(entry.vars as { uncompressedSize?: number }).uncompressedSize ??
entry.vars.compressedSize,
});
}
return true;
}
@@ -95,7 +102,11 @@ export class ZipArchiveResponse implements ReadTreeResponse {
if (this.shouldBeIncluded(entry)) {
files.push({
path: this.getInnerPath(stripFirstDirectoryFromPath(entry.path)),
path: this.getInnerPath(
this.stripFirstDirectory
? stripFirstDirectoryFromPath(entry.path)
: entry.path,
),
content: () => entry.buffer(),
});
} else {
@@ -144,7 +155,9 @@ 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(
stripFirstDirectoryFromPath(entry.path),
this.stripFirstDirectory
? stripFirstDirectoryFromPath(entry.path)
: entry.path,
);
const dirname = platformPath.dirname(entryPath);
if (dirname) {
+2 -2
View File
@@ -104,7 +104,7 @@ export type ReadTreeOptions = {
*
* If no filter is provided all files are extracted.
*/
filter?(path: string): boolean;
filter?(path: string, info?: { size: number }): boolean;
/**
* An etag can be provided to check whether readTree's response has changed from a previous execution.
@@ -164,7 +164,7 @@ export type FromArchiveOptions = {
// etag of the blob
etag: string;
// Filter passed on from the ReadTreeOptions
filter?: (path: string) => boolean;
filter?: (path: string, info?: { size: number }) => boolean;
};
export interface ReadTreeResponseFactory {
+8
View File
@@ -1,5 +1,13 @@
# @backstage/backend-test-utils
## 0.1.6
### Patch Changes
- Updated dependencies
- @backstage/backend-common@0.9.0
- @backstage/config@0.1.8
## 0.1.5
### Patch Changes
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-test-utils",
"description": "Test helpers library for Backstage backends",
"version": "0.1.5",
"version": "0.1.6",
"main": "src/index.ts",
"types": "src/index.ts",
"private": false,
@@ -30,9 +30,9 @@
"clean": "backstage-cli clean"
},
"dependencies": {
"@backstage/backend-common": "^0.8.9",
"@backstage/backend-common": "^0.9.0",
"@backstage/cli": "^0.7.8",
"@backstage/config": "^0.1.5",
"@backstage/config": "^0.1.8",
"knex": "^0.95.1",
"mysql2": "^2.2.5",
"pg": "^8.3.0",
+27
View File
@@ -1,5 +1,32 @@
# example-backend
## 0.2.43
### Patch Changes
- Updated dependencies
- @backstage/backend-common@0.9.0
- @backstage/plugin-catalog-backend@0.13.5
- @backstage/plugin-search-backend-module-pg@0.1.3
- @backstage/plugin-auth-backend@0.3.23
- @backstage/plugin-scaffolder-backend@0.15.2
- @backstage/integration@0.6.2
- @backstage/config@0.1.8
- @backstage/plugin-kubernetes-backend@0.3.15
- @backstage/plugin-techdocs-backend@0.10.0
- @backstage/plugin-jenkins-backend@0.1.4
- @backstage/plugin-app-backend@0.3.16
- @backstage/plugin-badges-backend@0.1.9
- @backstage/plugin-code-coverage-backend@0.1.10
- @backstage/plugin-graphql-backend@0.1.9
- @backstage/plugin-kafka-backend@0.2.9
- @backstage/plugin-proxy-backend@0.2.12
- @backstage/plugin-rollbar-backend@0.1.14
- @backstage/plugin-scaffolder-backend-module-rails@0.1.5
- @backstage/plugin-search-backend@0.2.5
- @backstage/plugin-todo-backend@0.1.11
- example-app@0.2.43
## 0.2.41
### Patch Changes
+24 -22
View File
@@ -1,6 +1,6 @@
{
"name": "example-backend",
"version": "0.2.41",
"version": "0.2.43",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -27,40 +27,42 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"@backstage/backend-common": "^0.8.9",
"@backstage/backend-common": "^0.9.0",
"@backstage/catalog-client": "^0.3.17",
"@backstage/catalog-model": "^0.9.0",
"@backstage/config": "^0.1.5",
"@backstage/integration": "^0.6.0",
"@backstage/plugin-app-backend": "^0.3.15",
"@backstage/plugin-auth-backend": "^0.3.20",
"@backstage/plugin-badges-backend": "^0.1.8",
"@backstage/plugin-catalog-backend": "^0.13.2",
"@backstage/plugin-code-coverage-backend": "^0.1.9",
"@backstage/plugin-graphql-backend": "^0.1.8",
"@backstage/plugin-jenkins-backend": "^0.1.2",
"@backstage/plugin-kubernetes-backend": "^0.3.14",
"@backstage/plugin-kafka-backend": "^0.2.8",
"@backstage/plugin-proxy-backend": "^0.2.9",
"@backstage/plugin-rollbar-backend": "^0.1.13",
"@backstage/plugin-scaffolder-backend": "^0.15.0",
"@backstage/plugin-scaffolder-backend-module-rails": "^0.1.4",
"@backstage/plugin-search-backend": "^0.2.3",
"@backstage/config": "^0.1.8",
"@backstage/integration": "^0.6.2",
"@backstage/plugin-app-backend": "^0.3.16",
"@backstage/plugin-auth-backend": "^0.3.23",
"@backstage/plugin-badges-backend": "^0.1.9",
"@backstage/plugin-catalog-backend": "^0.13.5",
"@backstage/plugin-code-coverage-backend": "^0.1.10",
"@backstage/plugin-graphql-backend": "^0.1.9",
"@backstage/plugin-jenkins-backend": "^0.1.4",
"@backstage/plugin-kubernetes-backend": "^0.3.15",
"@backstage/plugin-kafka-backend": "^0.2.9",
"@backstage/plugin-proxy-backend": "^0.2.12",
"@backstage/plugin-rollbar-backend": "^0.1.14",
"@backstage/plugin-scaffolder-backend": "^0.15.2",
"@backstage/plugin-scaffolder-backend-module-rails": "^0.1.5",
"@backstage/plugin-search-backend": "^0.2.5",
"@backstage/plugin-search-backend-node": "^0.4.0",
"@backstage/plugin-search-backend-module-elasticsearch": "^0.0.2",
"@backstage/plugin-search-backend-module-pg": "^0.1.1",
"@backstage/plugin-techdocs-backend": "^0.9.2",
"@backstage/plugin-todo-backend": "^0.1.9",
"@backstage/plugin-search-backend-module-pg": "^0.1.3",
"@backstage/plugin-techdocs-backend": "^0.10.0",
"@backstage/plugin-todo-backend": "^0.1.11",
"@gitbeaker/node": "^30.2.0",
"@octokit/rest": "^18.5.3",
"azure-devops-node-api": "^10.2.2",
"dockerode": "^3.2.1",
"example-app": "^0.2.41",
"example-app": "^0.2.43",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"express-prom-bundle": "^6.3.6",
"knex": "^0.95.1",
"pg": "^8.3.0",
"pg-connection-string": "^2.3.0",
"prom-client": "^13.2.0",
"sqlite3": "^5.0.1",
"winston": "^3.2.1"
},
+3
View File
@@ -36,6 +36,7 @@ import {
} from '@backstage/backend-common';
import { Config } from '@backstage/config';
import healthcheck from './plugins/healthcheck';
import { metricsInit, metricsHandler } from './metrics';
import auth from './plugins/auth';
import catalog from './plugins/catalog';
import codeCoverage from './plugins/codecoverage';
@@ -72,6 +73,7 @@ function makeCreateEnv(config: Config) {
}
async function main() {
metricsInit();
const logger = getRootLogger();
logger.info(
@@ -124,6 +126,7 @@ async function main() {
const service = createServiceBuilder(module)
.loadConfig(config)
.addRouter('', await healthcheck(healthcheckEnv))
.addRouter('', metricsHandler())
.addRouter('/api', apiRouter)
.addRouter('', await app(appEnv));
+63
View File
@@ -0,0 +1,63 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useHotCleanup } from '@backstage/backend-common';
import { RequestHandler, Request } from 'express';
import promBundle from 'express-prom-bundle';
import prom from 'prom-client';
import * as url from 'url';
/**
* Experimental Prometheus metrics used to benchmark the performance of the
* software catalog. Use this at your own risk.
*/
const rootRegEx = new RegExp('^/([^/]*)/.*');
const apiRegEx = new RegExp('^/api/([^/]*)/.*');
function normalizePath(req: Request): string {
const path = url.parse(req.originalUrl || req.url).pathname || '/';
// Capture /api/ and the plugin name
if (apiRegEx.test(path)) {
return path.replace(apiRegEx, '/api/$1');
}
// Only the first path segment at root level
return path.replace(rootRegEx, '/$1');
}
export function metricsInit(): void {
prom.collectDefaultMetrics({ prefix: 'backstage_' });
}
/**
* Adds a /metrics endpoint, register default runtime metrics and instrument the router.
*/
export function metricsHandler(): RequestHandler {
// We can only initialize the metrics once and have to clean them up between hot reloads
useHotCleanup(module, () => prom.register.clear());
return promBundle({
includeMethod: true,
includePath: true,
// Using includePath alone is problematic, as it will include path labels with high
// cardinality (e.g. path params). Instead we would have to template them. However, this
// is difficult, as every backend plugin might use different routes. Instead we only take
// the first directory of the path, to have at least an idea how each plugin performs:
normalizePath,
promClient: { collectDefaultMetrics: {} },
});
}
+1 -4
View File
@@ -230,6 +230,7 @@ export type EntityMeta = JsonObject & {
generation?: number;
name: string;
namespace?: string;
title?: string;
description?: string;
labels?: Record<string, string>;
annotations?: Record<string, string>;
@@ -722,10 +723,6 @@ export interface TemplateEntityV1beta2 extends Entity {
// (undocumented)
kind: 'Template';
// (undocumented)
metadata: EntityMeta & {
title?: string;
};
// (undocumented)
spec: {
type: string;
parameters?: JsonObject | JsonObject[];
+24 -1
View File
@@ -103,7 +103,13 @@ export type EntityMeta = JsonObject & {
* The name of the entity.
*
* Must be unique within the catalog at any given point in time, for any
* given namespace + kind pair.
* given namespace + kind pair. This value is part of the technical
* identifier of the entity, and as such it will appear in URLs, database
* tables, entity references, and similar. It is subject to restrictions
* regarding what characters are allowed.
*
* If you want to use a different, more human readable string with fewer
* restrictions on it in user interfaces, see the `title` field below.
*/
name: string;
@@ -112,6 +118,23 @@ export type EntityMeta = JsonObject & {
*/
namespace?: string;
/**
* A display name of the entity, to be presented in user interfaces instead
* of the `name` property above, when available.
*
* This field is sometimes useful when the `name` is cumbersome or ends up
* being perceived as overly technical. The title generally does not have
* as stringent format requirements on it, so it may contain special
* characters and be more explanatory. Do keep it very short though, and
* avoid situations where a title can be confused with the name of another
* entity, or where two entities share a title.
*
* Note that this is only for display purposes, and may be ignored by some
* parts of the code. Entity references still always make use of the `name`
* property, not the title.
*/
title?: string;
/**
* A short (typically relatively few words, on one line) description of the
* entity.
@@ -15,16 +15,13 @@
*/
import { JsonObject } from '@backstage/config';
import type { Entity, EntityMeta } from '../entity/Entity';
import type { Entity } from '../entity/Entity';
import schema from '../schema/kinds/Template.v1beta2.schema.json';
import { ajvCompiledJsonSchemaValidator } from './util';
export interface TemplateEntityV1beta2 extends Entity {
apiVersion: 'backstage.io/v1beta2';
kind: 'Template';
metadata: EntityMeta & {
title?: string;
};
spec: {
type: string;
parameters?: JsonObject | JsonObject[];
@@ -53,6 +53,12 @@
"examples": ["default", "admin"],
"minLength": 1
},
"title": {
"type": "string",
"description": "A display name of the entity, to be presented in user interfaces instead of the name property, when available.",
"examples": ["React SSR Template"],
"minLength": 1
},
"description": {
"type": "string",
"description": "A short (typically relatively few words, on one line) description of the entity."
@@ -69,17 +69,6 @@
"kind": {
"enum": ["Template"]
},
"metadata": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "The nice display name for the template.",
"examples": ["React SSR Template"],
"minLength": 1
}
}
},
"spec": {
"type": "object",
"required": ["type", "steps"],
@@ -30,6 +30,8 @@ describe('entitySchemaValidator', () => {
generation: 13,
name: 'test',
namespace: 'ns',
title: 'My Component, Yay',
description: 'Yeah this is probably the best component so far',
labels: {
'backstage.io/custom': 'ValueStuff',
},
@@ -192,6 +194,21 @@ describe('entitySchemaValidator', () => {
expect(() => validator(entity)).toThrow(/namespace/);
});
it('accepts missing title', () => {
delete entity.metadata.title;
expect(() => validator(entity)).not.toThrow();
});
it('rejects bad title type', () => {
entity.metadata.title = 7;
expect(() => validator(entity)).toThrow(/title/);
});
it('rejects empty title', () => {
entity.metadata.title = '';
expect(() => validator(entity)).toThrow(/title/);
});
it('accepts missing description', () => {
delete entity.metadata.description;
expect(() => validator(entity)).not.toThrow();
+15
View File
@@ -1,5 +1,20 @@
# @backstage/cli
## 0.7.9
### Patch Changes
- f3bba3d2b: Remove debug logging
- 8ea1e96b3: Fix file path handling in diff commands on Windows.
- 2518aab58: Compensate for error formatting mismatch between Webpack 5 and react-dev-utils
- 1ac2961c3: Reintroduce Node.js shims that were removed in the Webpack 5 migration.
- 8d07a8b03: Add Buffer to `ProvidePlugin` since this is no longer provided in `webpack@5`
- fe506a0cf: Remove Webpack deprecation message when running build.
- 485438a56: Fix `backstage-cli backend:dev` argument passing
- Updated dependencies
- @backstage/config@0.1.7
- @backstage/config-loader@0.6.7
## 0.7.8
### Patch Changes
+11 -9
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
"version": "0.7.8",
"version": "0.7.9",
"private": false,
"publishConfig": {
"access": "public"
@@ -31,8 +31,8 @@
"@babel/core": "^7.4.4",
"@babel/plugin-transform-modules-commonjs": "^7.4.4",
"@backstage/cli-common": "^0.1.2",
"@backstage/config": "^0.1.6",
"@backstage/config-loader": "^0.6.6",
"@backstage/config": "^0.1.8",
"@backstage/config-loader": "^0.6.7",
"@hot-loader/react-dom": "^16.13.0",
"@lerna/package-graph": "^4.0.0",
"@lerna/project": "^4.0.0",
@@ -56,6 +56,7 @@
"@yarnpkg/lockfile": "^1.1.0",
"babel-plugin-dynamic-import-node": "^2.3.3",
"bfj": "^7.0.2",
"buffer": "^6.0.3",
"chalk": "^4.0.0",
"chokidar": "^3.3.1",
"commander": "^6.1.0",
@@ -84,6 +85,7 @@
"json-schema": "^0.3.0",
"lodash": "^4.17.19",
"mini-css-extract-plugin": "^1.4.1",
"node-libs-browser": "^2.2.1",
"ora": "^5.3.0",
"postcss": "^8.1.0",
"process": "^0.11.10",
@@ -118,14 +120,14 @@
"yn": "^4.0.0"
},
"devDependencies": {
"@backstage/backend-common": "^0.8.9",
"@backstage/config": "^0.1.6",
"@backstage/core-components": "^0.3.1",
"@backstage/backend-common": "^0.9.0",
"@backstage/config": "^0.1.8",
"@backstage/core-components": "^0.3.3",
"@backstage/core-plugin-api": "^0.1.6",
"@backstage/core-app-api": "^0.1.8",
"@backstage/dev-utils": "^0.2.6",
"@backstage/core-app-api": "^0.1.10",
"@backstage/dev-utils": "^0.2.7",
"@backstage/test-utils": "^0.1.17",
"@backstage/theme": "^0.2.9",
"@backstage/theme": "^0.2.10",
"@types/diff": "^5.0.0",
"@types/express": "^4.17.6",
"@types/fs-extra": "^9.0.1",
-1
View File
@@ -28,7 +28,6 @@ export async function serveBackend(options: BackendServeOptions) {
const compiler = webpack(config, (err: Error | undefined) => {
if (err) {
console.log('here');
console.error(err);
} else console.log('Build succeeded');
});
+16 -3
View File
@@ -117,9 +117,22 @@ async function build(compiler: webpack.Compiler, isCi: boolean) {
if (!stats) {
throw new Error('No stats provided');
}
const { errors, warnings } = formatWebpackMessages(
stats.toJson({ all: false, warnings: true, errors: true }),
);
const serializedStats = stats.toJson({
all: false,
warnings: true,
errors: true,
});
// NOTE(freben): The code below that extracts the message part of the errors,
// is due to react-dev-utils not yet being compatible with webpack 5. This
// may be possible to remove (just passing the serialized stats object
// directly into the format function) after a new release of react-dev-utils
// has been made available.
// See https://github.com/facebook/create-react-app/issues/9880
const { errors, warnings } = formatWebpackMessages({
errors: serializedStats.errors?.map(e => (e.message ? e.message : e)),
warnings: serializedStats.warnings?.map(e => (e.message ? e.message : e)),
});
if (errors.length) {
// Only keep the first error. Others are often indicative
+5 -1
View File
@@ -32,6 +32,7 @@ import { BundlingOptions, BackendBundlingOptions, LernaPackage } from './types';
import { version } from '../../lib/version';
import { paths as cliPaths } from '../../lib/paths';
import { runPlain } from '../run';
import pickBy from 'lodash/pickBy';
export function resolveBaseUrl(config: Config): URL {
const baseUrl = config.getString('app.baseUrl');
@@ -119,6 +120,7 @@ export async function createConfig(
plugins.push(
new ProvidePlugin({
process: 'process/browser',
Buffer: ['buffer', 'Buffer'],
}),
);
@@ -176,6 +178,7 @@ export async function createConfig(
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
mainFields: ['browser', 'module', 'main'],
fallback: {
...pickBy(require('node-libs-browser')),
module: false,
dgram: false,
dns: false,
@@ -208,7 +211,7 @@ export async function createConfig(
output: {
path: paths.targetDist,
publicPath: validBaseUrl.pathname,
filename: isDev ? '[name].js' : 'static/[name].[hash:8].js',
filename: isDev ? '[name].js' : 'static/[name].[fullhash:8].js',
chunkFilename: isDev
? '[name].chunk.js'
: 'static/[name].[chunkhash:8].chunk.js',
@@ -317,6 +320,7 @@ export async function createBackendConfig(
new RunScriptWebpackPlugin({
name: 'main.js',
nodeArgs: options.inspectEnabled ? ['--inspect'] : undefined,
args: process.argv.slice(3), // drop `node backstage-cli backend:dev`
}),
new webpack.HotModuleReplacementPlugin(),
...(checksEnabled
+2 -1
View File
@@ -16,6 +16,7 @@
import chalk from 'chalk';
import { diffLines } from 'diff';
import { sep, posix } from 'path';
import { FileDiff, PromptFunc, FileHandler, WriteFileFunc } from './types';
function sortObjectKeys(obj: Record<string, unknown>) {
@@ -281,7 +282,7 @@ export async function handleAllFiles(
promptFunc: PromptFunc,
) {
for (const file of files) {
const { path } = file;
const path = file.path.split(sep).join(posix.sep);
const fileHandler = fileHandlers.find(handler =>
handler.patterns.some(pattern =>
typeof pattern === 'string' ? pattern === path : pattern.test(path),
+16
View File
@@ -1,5 +1,21 @@
# @backstage/codemods
## 0.1.11
### Patch Changes
- Updated dependencies
- @backstage/core-app-api@0.1.10
- @backstage/core-components@0.3.3
## 0.1.10
### Patch Changes
- Updated dependencies
- @backstage/core-app-api@0.1.9
- @backstage/core-components@0.3.2
## 0.1.9
### Patch Changes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/codemods",
"description": "A collection of codemods for Backstage projects",
"version": "0.1.9",
"version": "0.1.11",
"private": false,
"publishConfig": {
"access": "public",
+9
View File
@@ -1,5 +1,14 @@
# @backstage/config-loader
## 0.6.7
### Patch Changes
- 0ade9d02b: Include `devDependencies` and `optionalDependencies` in the detection of Backstage packages when collecting configuration schema.
- 9b8cec063: Add support for config file watching through a new group of `watch` options to `loadConfig`.
- Updated dependencies
- @backstage/config@0.1.7
## 0.6.6
### Patch Changes
+4
View File
@@ -37,6 +37,10 @@ export type LoadConfigOptions = {
configPaths: string[];
env?: string;
experimentalEnvFunc?: EnvFunc;
watch?: {
onChange: (configs: AppConfig[]) => void;
stopSignal?: Promise<void>;
};
};
// Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts
+3 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
"version": "0.6.6",
"version": "0.6.7",
"private": false,
"publishConfig": {
"access": "public",
@@ -31,9 +31,10 @@
},
"dependencies": {
"@backstage/cli-common": "^0.1.1",
"@backstage/config": "^0.1.6",
"@backstage/config": "^0.1.7",
"@types/json-schema": "^7.0.6",
"ajv": "^7.0.3",
"chokidar": "^3.5.2",
"fs-extra": "9.1.0",
"json-schema": "^0.3.0",
"json-schema-merge-allof": "^0.8.1",
@@ -87,6 +87,8 @@ describe('collectConfigSchemas', () => {
dependencies: {
c1: '0.0.0',
c2: '0.0.0',
},
devDependencies: {
'@backstage/mock': '0.0.0',
},
configSchema: { ...mockSchema, title: 'b' },
@@ -74,6 +74,8 @@ export async function collectConfigSchemas(
const pkg = await fs.readJson(pkgPath);
const depNames = [
...Object.keys(pkg.dependencies ?? {}),
...Object.keys(pkg.devDependencies ?? {}),
...Object.keys(pkg.optionalDependencies ?? {}),
...Object.keys(pkg.peerDependencies ?? {}),
];
+81 -2
View File
@@ -14,11 +14,13 @@
* limitations under the License.
*/
import { AppConfig } from '@backstage/config';
import { loadConfig } from './loader';
import mockFs from 'mock-fs';
import fs from 'fs-extra';
describe('loadConfig', () => {
beforeAll(() => {
beforeEach(() => {
process.env.MY_SECRET = 'is-secret';
process.env.SUBSTITUTE_ME = 'substituted';
@@ -63,7 +65,7 @@ describe('loadConfig', () => {
});
});
afterAll(() => {
afterEach(() => {
mockFs.restore();
});
@@ -170,4 +172,81 @@ describe('loadConfig', () => {
},
]);
});
it('watches config files', async () => {
const onChange = defer<AppConfig[]>();
const stopSignal = defer<void>();
await expect(
loadConfig({
configRoot: '/root',
configPaths: [],
watch: {
onChange: onChange.resolve,
stopSignal: stopSignal.promise,
},
}),
).resolves.toEqual([
{
context: 'app-config.yaml',
data: {
app: {
title: 'Example App',
sessionKey: 'abc123',
escaped: '${Escaped}',
},
},
},
]);
await fs.writeJson('/root/app-config.yaml', {
app: {
title: 'New Title',
},
});
await expect(onChange.promise).resolves.toEqual([
{
context: 'app-config.yaml',
data: {
app: {
title: 'New Title',
},
},
},
]);
stopSignal.resolve();
});
it('stops watching config files', async () => {
const stopSignal = defer<void>();
await loadConfig({
configRoot: '/root',
configPaths: [],
watch: {
onChange: () => {
expect('not').toBe('called');
},
stopSignal: stopSignal.promise,
},
});
stopSignal.resolve();
await fs.writeJson('/root/app-config.yaml', {
app: {
title: 'New Title',
},
});
await new Promise(resolve => setTimeout(resolve, 1000));
});
function defer<T>() {
let resolve: (value: T) => void;
const promise = new Promise<T>(_resolve => {
resolve = _resolve;
});
return { promise, resolve: resolve! };
}
});

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