Merge branch 'master' into techdoc_title
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Allow to pass custom TemplateCard to ScaffolderPage
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
GitHub discovery processor passes over repositories that do not have a default branch
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
GitHub create repository webhook action: `github:webhook` for Backstage plugin Scaffolder has been added.
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
---
|
||||
|
||||
Fix importing yaml files from URLs with trailing query parameters.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-backend': patch
|
||||
---
|
||||
|
||||
Adds optional namespacing for users in the GitHub Multi Org Plugin
|
||||
@@ -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`
|
||||
@@ -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
|
||||
@@ -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 />}>
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-org': patch
|
||||
---
|
||||
|
||||
Don't open a new tab when clicking on the ownership card.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-jenkins': patch
|
||||
---
|
||||
|
||||
Make `isJenkinsAvailable` recognise both the old and the new Jenkins annotation.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-auth-backend': patch
|
||||
---
|
||||
|
||||
typo fix `tenentId` in Azure auth provider docs
|
||||
@@ -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).
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-shortcuts': patch
|
||||
---
|
||||
|
||||
Fix material-ui warning about deprecated `onExit` prop
|
||||
@@ -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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/create-app': patch
|
||||
---
|
||||
|
||||
Minor tweaks to the create-app template to match earlier documented changes
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Export type that are needed to implement a new `UrlReader`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-sonarqube': patch
|
||||
---
|
||||
|
||||
Update README to show a simpler proxy configuration.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/techdocs-common': patch
|
||||
---
|
||||
|
||||
Force using `posix` path for cloud storage
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Use a `Link` for the edit button on the `AboutCard` instead of doing `window.open(...)`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-org': patch
|
||||
---
|
||||
|
||||
Fix OwnershipCard links to support namespaced owners
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 team’s 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
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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** -
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
```
|
||||
|
||||
@@ -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'
|
||||
```
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -104,6 +104,11 @@
|
||||
],
|
||||
"Integrations": [
|
||||
"integrations/index",
|
||||
{
|
||||
"type": "subcategory",
|
||||
"label": "AWS S3",
|
||||
"ids": ["integrations/aws-s3/locations"]
|
||||
},
|
||||
{
|
||||
"type": "subcategory",
|
||||
"label": "Azure",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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: {} },
|
||||
});
|
||||
}
|
||||
@@ -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[];
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ?? {}),
|
||||
];
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user