diff --git a/.changeset/1724.md b/.changeset/1724.md new file mode 100644 index 0000000000..2ac03f94e9 --- /dev/null +++ b/.changeset/1724.md @@ -0,0 +1,27 @@ +--- +'@backstage/cli': minor +'@backstage/plugin-api-docs': minor +'@backstage/plugin-app-backend': minor +'@backstage/plugin-auth-backend': minor +'@backstage/plugin-catalog-graphql': minor +'@backstage/plugin-catalog': minor +'@backstage/plugin-circleci': minor +'@backstage/plugin-explore': minor +'@backstage/plugin-gcp-projects': minor +'@backstage/plugin-github-actions': minor +'@backstage/plugin-gitops-profiles': minor +'@backstage/plugin-graphiql': minor +'@backstage/plugin-jenkins': minor +'@backstage/plugin-kubernetes': minor +'@backstage/plugin-lighthouse': minor +'@backstage/plugin-newrelic': minor +'@backstage/plugin-register-component': minor +'@backstage/plugin-rollbar': minor +'@backstage/plugin-scaffolder': minor +'@backstage/plugin-sentry': minor +'@backstage/plugin-tech-radar': minor +'@backstage/plugin-techdocs': minor +'@backstage/plugin-welcome': minor +--- + +Create backend plugin through CLI diff --git a/.changeset/2284.md b/.changeset/2284.md new file mode 100644 index 0000000000..655cc5a3e8 --- /dev/null +++ b/.changeset/2284.md @@ -0,0 +1,11 @@ +--- +'@backstage/core-api': minor +'@backstage/core': minor +'@backstage/plugin-auth-backend': minor +--- + +Add SAML login to backstage + +![](https://user-images.githubusercontent.com/872486/92251660-bb9e3400-eeff-11ea-86fe-1f2a0262cd31.png) + +![](https://user-images.githubusercontent.com/872486/93851658-1a76f200-fce3-11ea-990b-26ca1a327a15.png) diff --git a/.changeset/2515.md b/.changeset/2515.md new file mode 100644 index 0000000000..32f5edd6b7 --- /dev/null +++ b/.changeset/2515.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cloudbuild': minor +--- + +Releasing Google Cloud Build Plugin diff --git a/.changeset/2532.md b/.changeset/2532.md new file mode 100644 index 0000000000..14307f6c08 --- /dev/null +++ b/.changeset/2532.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': minor +--- + +Add handling and docs for entity references diff --git a/.changeset/2535.md b/.changeset/2535.md new file mode 100644 index 0000000000..bd28f3ce03 --- /dev/null +++ b/.changeset/2535.md @@ -0,0 +1,14 @@ +--- +'@backstage/core': patch +'@backstage/theme': patch +--- + +Fix banner position and color + +This PR closes: #2245 + +The "fixed" props added to control the position of the banner. When it is set to true the banner will be shown in bottom of that page and the width will be based on the content of the message. + +![](https://user-images.githubusercontent.com/15106494/93765685-999df480-fc15-11ea-8fa5-11cac5836cf1.png) + +![](https://user-images.githubusercontent.com/15106494/93765697-9e62a880-fc15-11ea-92af-b6a7fee4bb21.png) diff --git a/.changeset/2541.md b/.changeset/2541.md new file mode 100644 index 0000000000..1ba29da040 --- /dev/null +++ b/.changeset/2541.md @@ -0,0 +1,5 @@ +--- +'@backstage/theme': minor +--- + +Tweak dark mode colors diff --git a/.changeset/2543.md b/.changeset/2543.md new file mode 100644 index 0000000000..00311ade45 --- /dev/null +++ b/.changeset/2543.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Add Azure DevOps support to the scaffolder backend + +This adds support for Azure DevOps to the scaffolder (preparer & publisher). I thought I should get this in there now since #2426 has been merged. I had a previous PR with only the preparer but I closed that in favor of this one. + +I stayed with the 'azure/api' structure but I guess we should try and go the same way as with GitHub here #2501 diff --git a/.changeset/2562.md b/.changeset/2562.md new file mode 100644 index 0000000000..716f370ad9 --- /dev/null +++ b/.changeset/2562.md @@ -0,0 +1,7 @@ +--- +'@backstage/create-app': minor +'@backstage/plugin-auth-backend': minor +'@backstage/plugin-techdocs-backend': minor +--- + +Change the default backend plugin mount point to /api diff --git a/.changeset/2563.md b/.changeset/2563.md new file mode 100644 index 0000000000..318d5cf5ef --- /dev/null +++ b/.changeset/2563.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-actions': minor +--- + +Adds a widget to show recent git workflow runs to the github actions plugin. The default setting is the last 5 runs across all branches but both branch and the number of runs are configurable. diff --git a/.changeset/2565.md b/.changeset/2565.md new file mode 100644 index 0000000000..92253101ee --- /dev/null +++ b/.changeset/2565.md @@ -0,0 +1,59 @@ +--- +'@backstage/core-api': patch +--- + +Add initial RouteRefRegistry + +Starting out some work to bring routing back and working as part of the work towards finalizing #1536 + +This is some of the groundwork of an experiment we're working on to enable routing via RouteRefs, while letting the app itself look something like this: + +```jsx +const App = () => ( + + + + {' '} + // catalogRouteRef + + + + + + + // statusRouteRef + + + + + + + + + + // sentryRouteRef + + + + + + + + + + + + + + + +); +``` + +As part of inverting the composition of the app, route refs and routing in general was somewhat broken, intentionally. Right now it's not really possible to easily route to different parts of the app from a plugin, or even different parts of the plugin that are not within the same router. + +The core part of the experiment is to construct a map of ApiRef[] -> path overrides. Each key in the map is the list of route refs to traversed to reach a leaf in the routing tree, and the value is the path override at that point. For example, the above tree would add entries like [techDocsRouteRef] -> '/docs', and [entityRouteRef, apiDocsRouteRef] -> '/api'. By mapping out the entire app in this structure, the idea is that we can navigate to any point in the app using RouteRefs. + +The RouteRefRegistry is an implementation of such a map, and the idea is to add it in master to make it a bit easier to experiment and iterate. This is not an exposed API at this point. + +We've explored a couple of alternatives for how to enable routing, but it's boiled down to either a solution centred around the route map mentioned above, or treating all routes as static and globally unique, with no room for flexibility, customization or conflicts between different plugins. We're starting out pursuing this options 😁. We also expect that a the app-wide routing table will make things like dynamic loading a lot cleaner, as there would be a much more clear handoff between the main chunk and dynamic chunks. diff --git a/.changeset/2575.md b/.changeset/2575.md new file mode 100644 index 0000000000..b08f50ed13 --- /dev/null +++ b/.changeset/2575.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': minor +--- + +Fix dense in Structured Metadata Table diff --git a/.changeset/2586.md b/.changeset/2586.md new file mode 100644 index 0000000000..568adb8a3e --- /dev/null +++ b/.changeset/2586.md @@ -0,0 +1,6 @@ +--- +'@backstage/catalog-model': minor +'@backstage/plugin-catalog-backend': minor +--- + +Entirely case insensitive read path of entities diff --git a/.changeset/2587.md b/.changeset/2587.md new file mode 100644 index 0000000000..910de1ca04 --- /dev/null +++ b/.changeset/2587.md @@ -0,0 +1,9 @@ +--- +'@backstage/catalog-model': minor +--- + +Add the User & Group entities + +A user describes a person, such as an employee, a contractor, or similar. Users belong to Group entities in the catalog. + +A group describes an organizational entity, such as for example a team, a business unit, or a loose collection of people in an interest group. Members of these groups are modeled in the catalog as kind User. diff --git a/.changeset/2597.md b/.changeset/2597.md new file mode 100644 index 0000000000..9db25e8d0e --- /dev/null +++ b/.changeset/2597.md @@ -0,0 +1,6 @@ +--- +'@backstage/catalog-model': minor +'@backstage/plugin-catalog-backend': minor +--- + +Add ApiDefinitionAtLocationProcessor that allows to load a API definition from another location diff --git a/.changeset/2598.md b/.changeset/2598.md new file mode 100644 index 0000000000..03e10e7db8 --- /dev/null +++ b/.changeset/2598.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-techdocs': minor +--- + +Add a message if techdocs takes long time to load + +Fixes #2416. + +The UI after the change should look like this: + +![techdocs-progress-bar](https://user-images.githubusercontent.com/33940798/94189286-296ac980-fec8-11ea-9051-1b3db938d12f.gif) diff --git a/.changeset/2600.md b/.changeset/2600.md new file mode 100644 index 0000000000..6fc9775fa1 --- /dev/null +++ b/.changeset/2600.md @@ -0,0 +1,19 @@ +--- +'@backstage/plugin-techdocs-backend': minor +'@backstage/plugin-proxy-backend': minor +'@backstage/plugin-auth-backend': minor +'@backstage/create-app': minor +'@backstage/backend-common': minor +--- + +Add service discovery interface and implement for single host deployments + +Fixes #1847, #2596 + +Went with an interface similar to the frontend DiscoveryApi, since it's dead simple but still provides a lot of flexibility in the implementation. + +Also ended up with two different methods, one for internal endpoint discovery and one for external. The two use-cases are explained a bit more in the docs, but basically it's service-to-service vs callback URLs. + +This did get me thinking about uniqueness and that we're heading towards a global namespace for backend plugin IDs. That's probably fine, but if we're happy with that we should leverage it a bit more to simplify the backend setup. For example we'd have each plugin provide its own ID and not manually mount on paths in the backend. + +Draft until we're happy with the implementation, then I can add more docs and changelog entry. Also didn't go on a thorough hunt for places where discovery can be used, but I don't think there are many since it's been pretty awkward to do service-to-service communication. diff --git a/.changeset/2603.md b/.changeset/2603.md new file mode 100644 index 0000000000..c24d2ea033 --- /dev/null +++ b/.changeset/2603.md @@ -0,0 +1,8 @@ +--- +'@backstage/backend-common': minor +'@backstage/create-app': minor +--- + +Make CSP configurable to fix app-backend served app not being able to fetch + +See discussion [here on discord](https://discordapp.com/channels/687207715902193673/687235481154617364/758721460163575850) diff --git a/.changeset/2606.md b/.changeset/2606.md new file mode 100644 index 0000000000..f6059e7705 --- /dev/null +++ b/.changeset/2606.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Move auth provider router creation to router diff --git a/.changeset/2609.md b/.changeset/2609.md new file mode 100644 index 0000000000..6bb8ab7a40 --- /dev/null +++ b/.changeset/2609.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Sync scaffolded backend with example diff --git a/.changeset/2610.md b/.changeset/2610.md new file mode 100644 index 0000000000..92ec8dcc0c --- /dev/null +++ b/.changeset/2610.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-circleci': minor +--- + +Refactor to use DiscoveryApi diff --git a/.changeset/2611.md b/.changeset/2611.md new file mode 100644 index 0000000000..cbf3129038 --- /dev/null +++ b/.changeset/2611.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Remove discovery api override diff --git a/.changeset/2612.md b/.changeset/2612.md new file mode 100644 index 0000000000..bf093d8f77 --- /dev/null +++ b/.changeset/2612.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-jenkins': patch +--- + +Refactor to use DiscoveryApi diff --git a/.changeset/2613.md b/.changeset/2613.md new file mode 100644 index 0000000000..cf8c1a155a --- /dev/null +++ b/.changeset/2613.md @@ -0,0 +1,12 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +Initial implementation of catalog user lookup + +This adds a basic catalog client + method for the Google provider to look up users in the catalog. It expects to find a single user entity in the catalog with a google.com/email annotation that matches the email of the Google profile. + +Right now it falls back to the old behavior of splitting the email, since I don't wanna break the sign-in flow for existing apps, not yet anyway x). + +- Added "@backstage/catalog-model@^0.1.1-alpha.23" as a dependency +- Added "node-fetch@^2.6.1" as a dependency diff --git a/.changeset/2614.md b/.changeset/2614.md new file mode 100644 index 0000000000..7e2660bc80 --- /dev/null +++ b/.changeset/2614.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': minor +--- + +Default to using internal scope for new plugins diff --git a/.changeset/2615.md b/.changeset/2615.md new file mode 100644 index 0000000000..412b8127c4 --- /dev/null +++ b/.changeset/2615.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': minor +--- + +Use localhost to fall back to IPv4 if IPv6 isn't available diff --git a/.changeset/2616.md b/.changeset/2616.md new file mode 100644 index 0000000000..9a70c0e79f --- /dev/null +++ b/.changeset/2616.md @@ -0,0 +1,7 @@ +--- +'@backstage/create-app': minor +--- + +Remove identity-backend + +Not used, and we're heading down the route of identities in the catalog diff --git a/.changeset/2623.md b/.changeset/2623.md new file mode 100644 index 0000000000..abd5ce7a4c --- /dev/null +++ b/.changeset/2623.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog': minor +--- + +Created EntityNotFound component for catalog which displays the 404 page when entity is not found. + +Fixes #2266 diff --git a/.changeset/2624.md b/.changeset/2624.md new file mode 100644 index 0000000000..dcfc800713 --- /dev/null +++ b/.changeset/2624.md @@ -0,0 +1,17 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Make title meaningful after component creation + +Fixes #2458. + +After the change, the UX should look like this: + +### If the component creation was successful: + +![successfully-created-component](https://user-images.githubusercontent.com/33940798/94339294-8bd1e000-0016-11eb-885b-7936fcc23b63.gif) + +### If the component creation failed: + +![failed-to-create-component](https://user-images.githubusercontent.com/33940798/94339296-90969400-0016-11eb-9a74-ce16b3dd8d88.gif) diff --git a/.changeset/2625-catalog-backend.md b/.changeset/2625-catalog-backend.md new file mode 100644 index 0000000000..bcd49dddb2 --- /dev/null +++ b/.changeset/2625-catalog-backend.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Add codeowners processor + +- Add `codeowners-utils@^1.0.2` as a dependency +- Add `core-js@^3.6.5` as a dependency +- Added new CodeOwnersProcessor diff --git a/.changeset/2625-cli.md b/.changeset/2625-cli.md new file mode 100644 index 0000000000..c5a5437b69 --- /dev/null +++ b/.changeset/2625-cli.md @@ -0,0 +1,7 @@ +--- +'@backstage/cli': patch +--- + +Add codeowners processor + +- Include ESNext.Promise in TypeScript compilation diff --git a/.changeset/2628.md b/.changeset/2628.md new file mode 100644 index 0000000000..0ac36d456a --- /dev/null +++ b/.changeset/2628.md @@ -0,0 +1,22 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +This feature works the same as \$secret does in config - it allows programmatic substitution of values into a document. + +This is particularly useful e.g. for API type entities where you do not want to repeat your entire API spec document inside the catalog-info.yaml file. For those cases, you can instead do something like + +``` +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: my-federated-service +spec: + type: graphql + definition: + $text: ./schema.graphql +``` + +The textual content of that file will be injected as the value of definition, during each refresh loop. Both relative and absolute paths are supported, as well as any HTTP/HTTPS URL pointing to a service that returns the relevant data. + +The initial version supports injection of text file data, and structured data from JSON and YAML files. You can add any handler of your own in addition to these. diff --git a/.changeset/2630.md b/.changeset/2630.md new file mode 100644 index 0000000000..933204e292 --- /dev/null +++ b/.changeset/2630.md @@ -0,0 +1,9 @@ +--- +'@backstage/create-app': minor +'@backstage/plugin-catalog-backend': minor +--- + +Allow node v14 and add to master build matrix + +- Upgrade sqlite3@^5.0.0 in @backstage/plugin-catalog-backend +- Add Node 14 to engines in @backstage/create-app diff --git a/.changeset/2637.md b/.changeset/2637.md new file mode 100644 index 0000000000..9d368634b6 --- /dev/null +++ b/.changeset/2637.md @@ -0,0 +1,21 @@ +--- +'@backstage/plugin-cost-insights': minor +'@backstage/plugin-explore': minor +--- + +This PR adds Spotify's Cost Insights Tool. Cost Insights explains costs from cloud services in an understandable way, using software terms familiar to your engineers. This tool helps you and your team make trade-offs between cost optimization efforts and your other priorities. + +Cost Insights features: + +Daily cost graph by team or billing account +Cost comparison against configurable business metrics +Insights panels for configurable cloud products your company uses +Cost alerts and recommendations +Selectable time periods for month over month, or quarter over quarter cost comparison +Conversion of cost growth into average engineer cost (configurable) to help optimization trade-off decisions + +![plugin-cost-insights](https://user-images.githubusercontent.com/3030003/94430416-e166d380-0161-11eb-891c-9ce10187683e.gif) + +This PR adds the Cost Insights frontend React plugin with a defined CostInsightsApi. We include an example client with static data in the expected format. This API should talk with a cloud billing backend that aggregates billing data from your cloud provider. + +Fixes #688 💵 diff --git a/.changeset/2639.md b/.changeset/2639.md new file mode 100644 index 0000000000..e6a623088e --- /dev/null +++ b/.changeset/2639.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': minor +--- + +Upgrade dependency `esbuild@0.7.7` diff --git a/.changeset/2641.md b/.changeset/2641.md new file mode 100644 index 0000000000..3b06358e81 --- /dev/null +++ b/.changeset/2641.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Simplify the read function in processors diff --git a/.changeset/2656.md b/.changeset/2656.md new file mode 100644 index 0000000000..e253b6b1f6 --- /dev/null +++ b/.changeset/2656.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +Lookup user in Google Auth Provider diff --git a/.changeset/2657.md b/.changeset/2657.md new file mode 100644 index 0000000000..9aa645d65f --- /dev/null +++ b/.changeset/2657.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': minor +--- + +Added EmptyState component diff --git a/.changeset/2660.md b/.changeset/2660.md new file mode 100644 index 0000000000..758d523f07 --- /dev/null +++ b/.changeset/2660.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Resolve some dark mode styling issues in asyncAPI specs diff --git a/.changeset/2661.md b/.changeset/2661.md new file mode 100644 index 0000000000..e73b0abf26 --- /dev/null +++ b/.changeset/2661.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Fixed banner component position in DismissableBanner component diff --git a/.changeset/2669-catalog-backend.md b/.changeset/2669-catalog-backend.md new file mode 100644 index 0000000000..32ee5c0f4d --- /dev/null +++ b/.changeset/2669-catalog-backend.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Add the ability to import users from GitHub Organization into the catalog. + +The token needs to have the scopes `user:email`, `read:user`, and `read:org`. diff --git a/.changeset/2669-catalog-model.md b/.changeset/2669-catalog-model.md new file mode 100644 index 0000000000..b9a2841a1b --- /dev/null +++ b/.changeset/2669-catalog-model.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': minor +--- + +Add the ability to import users from GitHub Organization into the catalog. diff --git a/.changeset/2669-create-app.md b/.changeset/2669-create-app.md new file mode 100644 index 0000000000..e7075ffeb8 --- /dev/null +++ b/.changeset/2669-create-app.md @@ -0,0 +1,7 @@ +--- +'@backstage/create-app': minor +--- + +Add the ability to import users from GitHub Organization into the catalog. + +The token needs to have the scopes `user:email`, `read:user`, and `read:org`. diff --git a/.changeset/2674.md b/.changeset/2674.md new file mode 100644 index 0000000000..15a7a0e849 --- /dev/null +++ b/.changeset/2674.md @@ -0,0 +1,12 @@ +--- +'@backstage/backend-common': minor +'@backstage/create-app': minor +--- + +Auto-create plugin databases + +Relates to #1598. + +This creates databases for plugins before handing off control to plugins. + +The list of plugins currently need to be hard-coded depending on the installed plugins. A later PR will properly refactor the code to provide a factory pattern where plugins specify what they need, and Knex instances will be provided based on the input. diff --git a/.changeset/2686.md b/.changeset/2686.md new file mode 100644 index 0000000000..8831c1309b --- /dev/null +++ b/.changeset/2686.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Update SSR template to pass CI diff --git a/.changeset/2689.md b/.changeset/2689.md new file mode 100644 index 0000000000..be75dd261b --- /dev/null +++ b/.changeset/2689.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Upgrade dependency rollup-plugin-typescript2 to ^0.27.3 diff --git a/.changeset/2722.md b/.changeset/2722.md new file mode 100644 index 0000000000..69642b5797 --- /dev/null +++ b/.changeset/2722.md @@ -0,0 +1,11 @@ +--- +'@backstage/plugin-api-docs': minor +--- + +There were some missing features and markdown was not rendered properly, but this is fixed now. + +Details: + +- [`asyncapi/asyncapi-react#149`](https://github.com/asyncapi/asyncapi-react/pull/149) - fix: improve markdown rendering of nested fields +- [`asyncapi/asyncapi-react#150`](https://github.com/asyncapi/asyncapi-react/pull/150) - feat: display the description of channels and operations +- [`asyncapi/asyncapi-react#153`](https://github.com/asyncapi/asyncapi-react/pull/153) - fix: let the list of `enums` break into multiple lines diff --git a/.changeset/2798.md b/.changeset/2798.md new file mode 100644 index 0000000000..2b202142ef --- /dev/null +++ b/.changeset/2798.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Use the new `UrlReader` in `PlaceholderProcessor`. +This allows to use the placeholder processor to include API definitions in API entities. +Previously it was only possible to do this if the definition comes from the same location type as the entity itself. diff --git a/.changeset/2803.md b/.changeset/2803.md new file mode 100644 index 0000000000..3a4773757f --- /dev/null +++ b/.changeset/2803.md @@ -0,0 +1,26 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Remove the backstage.io/definition-at-location annotation. +The annotation was superseded by the placeholder processor. + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: spotify + description: The Spotify web API + tags: + - spotify + - rest + annotations: + # Don't use this annotation, but the placeholder $text instead (see below). + backstage.io/definition-at-location: 'url:https://raw.githubusercontent.com/APIs-guru/openapi-directory/master/APIs/spotify.com/v1/swagger.yaml' +spec: + type: openapi + lifecycle: production + owner: spotify@example.com + definition: + $text: https://raw.githubusercontent.com/APIs-guru/openapi-directory/master/APIs/spotify.com/v1/swagger.yaml +``` diff --git a/.changeset/2804.md b/.changeset/2804.md new file mode 100644 index 0000000000..5223a24791 --- /dev/null +++ b/.changeset/2804.md @@ -0,0 +1,15 @@ +--- +'@backstage/plugin-proxy-backend': minor +--- + +Limit the http headers that are forwarded from the request to a safe set of defaults. +A user can configure additional headers that should be forwarded if the specific applications needs that. + +```yaml +proxy: + '/my-api': + target: 'https://my-api.com/get' + allowedHeaders: + # We need to forward the Authorization header that was provided by the caller + - Authorization +``` diff --git a/.changeset/codeowner-processor-url-reader.md b/.changeset/codeowner-processor-url-reader.md new file mode 100644 index 0000000000..af4eb0997f --- /dev/null +++ b/.changeset/codeowner-processor-url-reader.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Use the new `UrlReader` in the `CodeOwnersProcessor`. diff --git a/.changeset/config.json b/.changeset/config.json index c7c4f11e57..44a8523265 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -2,55 +2,7 @@ "$schema": "https://unpkg.com/@changesets/config@1.3.0/schema.json", "changelog": "@changesets/cli/changelog", "commit": false, - "linked": [ - [ - "example-app", - "@backstage/backend-common", - "example-backend", - "@backstage/catalog-model", - "@backstage/cli-common", - "@backstage/cli", - "@backstage/config-loader", - "@backstage/config", - "@backstage/core-api", - "@backstage/core", - "@backstage/create-app", - "@backstage/dev-utils", - "docgen", - "e2e-test", - "storybook", - "@techdocs/cli", - "@backstage/test-utils-core", - "@backstage/test-utils", - "@backstage/theme", - "@backstage/plugin-api-docs", - "@backstage/plugin-app-backend", - "@backstage/plugin-auth-backend", - "@backstage/plugin-catalog-backend", - "@backstage/plugin-catalog", - "@backstage/plugin-circleci", - "@backstage/plugin-gcp-projects", - "@backstage/plugin-github-actions", - "@backstage/plugin-gitops-profiles", - "@backstage/plugin-graphiql", - "@backstage/plugin-graphql-backend", - "@backstage/plugin-jenkins", - "@backstage/plugin-lighthouse", - "@backstage/plugin-newrelic", - "@backstage/plugin-proxy-backend", - "@backstage/plugin-register-component", - "@backstage/plugin-rollbar-backend", - "@backstage/plugin-rollbar", - "@backstage/plugin-scaffolder-backend", - "@backstage/plugin-scaffolder", - "@backstage/plugin-sentry-backend", - "@backstage/plugin-sentry", - "@backstage/plugin-tech-radar", - "@backstage/plugin-techdocs-backend", - "@backstage/plugin-techdocs", - "@backstage/plugin-welcome" - ] - ], + "linked": [["*"]], "access": "public", "baseBranch": "master", "updateInternalDependencies": "patch", diff --git a/.changeset/create-app-url-reader-update.md b/.changeset/create-app-url-reader-update.md new file mode 100644 index 0000000000..eb2e538b77 --- /dev/null +++ b/.changeset/create-app-url-reader-update.md @@ -0,0 +1,6 @@ +--- +'example-backend': patch +'@backstage/create-app': patch +--- + +Bump @backstage/catalog-backend and pass the now required UrlReader interface to the plugin diff --git a/.changeset/cyan-plants-dress.md b/.changeset/cyan-plants-dress.md new file mode 100644 index 0000000000..19ebc63e53 --- /dev/null +++ b/.changeset/cyan-plants-dress.md @@ -0,0 +1,5 @@ +--- +'example-backend': patch +--- + +Pass GitHub token into Scaffolder GitHub Preparer diff --git a/.changeset/good-islands-cheer.md b/.changeset/good-islands-cheer.md new file mode 100644 index 0000000000..2e7109b312 --- /dev/null +++ b/.changeset/good-islands-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': minor +--- + +Enable custom alert types in Cost Insights diff --git a/.changeset/long-ways-count.md b/.changeset/long-ways-count.md new file mode 100644 index 0000000000..e04c1225cc --- /dev/null +++ b/.changeset/long-ways-count.md @@ -0,0 +1,57 @@ +--- +'@backstage/plugin-techdocs-backend': minor +'@backstage/create-app': patch +--- + +Updated naming of environment variables. New pattern [NAME]\_TOKEN for Github, Gitlab, Azure & Github enterprise access tokens. + +### Detail: + +- Previously we have to export same token for both, catalog & scaffolder + +```bash +export GITHUB_ACCESS_TOKEN=foo +export GITHUB_PRIVATE_TOKEN=foo +``` + +with latest changes, only single export is sufficient. + +```bash +export GITHUB_TOKEN=foo +export GITLAB_TOKEN=foo +export GHE_TOKEN=foo +export AZURE_TOKEN=foo +``` + +### list: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Old nameNew name
GITHUB_ACCESS_TOKENGITHUB_TOKEN
GITHUB_PRIVATE_TOKENGITHUB_TOKEN
GITLAB_ACCESS_TOKENGITLAB_TOKEN
GITLAB_PRIVATE_TOKENGITLAB_TOKEN
AZURE_PRIVATE_TOKENAZURE_TOKEN
GHE_PRIVATE_TOKENGHE_TOKEN
diff --git a/.changeset/loud-lamps-visit.md b/.changeset/loud-lamps-visit.md new file mode 100644 index 0000000000..9023061643 --- /dev/null +++ b/.changeset/loud-lamps-visit.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Including source maps with all packages diff --git a/.changeset/new-url-reader.md b/.changeset/new-url-reader.md new file mode 100644 index 0000000000..94758c5bc1 --- /dev/null +++ b/.changeset/new-url-reader.md @@ -0,0 +1,12 @@ +--- +'@backstage/backend-common': patch +--- + +Added new UrlReader interface for reading opaque data from URLs with different providers. + +This new URL reading system is intended as a replacement for the various integrations towards +external systems in the catalog, scaffolder, and techdocs. It is configured via a new top-level +config section called 'integrations'. + +Along with the UrlReader interface is a new UrlReaders class, which exposes static factory +methods for instantiating readers that can read from many different integrations simultaneously. diff --git a/.changeset/ninety-pens-poke.md b/.changeset/ninety-pens-poke.md new file mode 100644 index 0000000000..94bcb06749 --- /dev/null +++ b/.changeset/ninety-pens-poke.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Fix for configured templates using 'url' locations even though it's not supported yet diff --git a/.changeset/pretty-cups-joke.md b/.changeset/pretty-cups-joke.md new file mode 100644 index 0000000000..49a1db2a75 --- /dev/null +++ b/.changeset/pretty-cups-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Add authentication token to Scaffolder GitHub Preparer diff --git a/.changeset/proud-eagles-grow.md b/.changeset/proud-eagles-grow.md new file mode 100644 index 0000000000..ef83a4ad26 --- /dev/null +++ b/.changeset/proud-eagles-grow.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixed duplicate help output, and print help on invalid command diff --git a/.changeset/remove-location-processor-read.md b/.changeset/remove-location-processor-read.md new file mode 100644 index 0000000000..78aea6d820 --- /dev/null +++ b/.changeset/remove-location-processor-read.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Remove the `read` argument of `LocationProcessor.processEntity`. +Instead, pass the `UrlReader` into the constructor of your `LocationProcessor`. diff --git a/.changeset/short-secrets.md b/.changeset/short-secrets.md new file mode 100644 index 0000000000..72cb059979 --- /dev/null +++ b/.changeset/short-secrets.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': minor +--- + +Added support for new shorthand when defining secrets, where `$env: ENV` can be used instead of `$secret: { env: ENV }` etc. diff --git a/.changeset/url-reader-processor.md b/.changeset/url-reader-processor.md new file mode 100644 index 0000000000..40fef3c106 --- /dev/null +++ b/.changeset/url-reader-processor.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +The catalog backend UrlReaderProcessor now uses a UrlReader from @backstage/backend-common, which must now be supplied to the constructor. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0437698301..de7ce773bb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -5,11 +5,9 @@ #### :heavy_check_mark: Checklist - + -- [ ] All tests are passing `yarn test` +- [ ] A changeset describing the change and affected packages. ([more info](https://github.com/spotify/backstage/blob/master/CONTRIBUTING.md#creating-changesets)) +- [ ] Added or updated documentation +- [ ] Tests for new functionality and regression tests for bug fixes - [ ] Screenshots attached (for UI changes) -- [ ] Relevant documentation updated -- [ ] Prettier run on changed files -- [ ] Tests added for new functionality -- [ ] Regression tests added for bug fixes diff --git a/.codecov.yml b/.github/codecov.yml similarity index 100% rename from .codecov.yml rename to .github/codecov.yml diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 21045f04dd..59a0eadb49 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -32,6 +32,7 @@ Codehilite codeowners config Config +configmaps configs const cookiecutter @@ -67,6 +68,7 @@ graphviz Hackathons haproxy heroku +horizontalpodautoscalers Hostname http https @@ -139,7 +141,9 @@ Raghunandan rankdir readme Readme +Recharts Redash +replicasets repo Repo repos diff --git a/.github/workflows/microsite-build-check.yml b/.github/workflows/microsite-build-check.yml index 0b614509f2..8a2fa99ed2 100644 --- a/.github/workflows/microsite-build-check.yml +++ b/.github/workflows/microsite-build-check.yml @@ -33,6 +33,10 @@ jobs: run: yarn install --frozen-lockfile working-directory: microsite + - name: prettier + run: yarn prettier:check + working-directory: microsite + - name: build microsite run: yarn build working-directory: microsite diff --git a/.imgbotconfig b/.imgbotconfig new file mode 100644 index 0000000000..f337e087dc --- /dev/null +++ b/.imgbotconfig @@ -0,0 +1,5 @@ +{ + "ignoredFiles": [ + "docs/assets/**/*.svg" + ] +} diff --git a/.prettierignore b/.prettierignore index 9e75b74eee..4b1acbb594 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,6 +1,6 @@ .yarn dist -microsite/build +microsite coverage *.hbs templates diff --git a/CHANGELOG.md b/CHANGELOG.md index bf2638cfe9..487b94a39d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re ### Backend (example-backend, or backends created with @backstage/create-app) +- A plugin database manager has been created, and plugins can now accept that interface as an argument during initialisation. Notably, the `auth` plugin has a [`createRouter` signature change](./plugins/auth-backend/src/service/router.ts). See [packages/backend/src/index.ts](./packages/backend/src/index.ts) on how to set it up. [#2697](https://github.com/spotify/backstage/pull/2697) + +## v0.1.1-alpha.24 + +### Backend (example-backend, or backends created with @backstage/create-app) + - The default mount point for backend plugins have been changed to `/api`. These changes are done in the backend package itself, so it is recommended that you sync up existing backend packages with this new pattern. [#2562](https://github.com/spotify/backstage/pull/2562) - A service discovery mechanism for backend plugins has been added, and is now a requirement for several backend plugins. See [packages/backend/src/index.ts](./packages/backend/src/index.ts) for how to set it up using `SingleHostDiscovery` from `@backstage/backend-common`. Note that the default base path for plugins is set to `/api` to that change, but it can be set to use the old behavior via the `basePath` option. [#2600](https://github.com/spotify/backstage/pull/2600) diff --git a/app-config.yaml b/app-config.yaml index 88df40813e..228eafc588 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -18,23 +18,20 @@ proxy: target: https://circleci.com/api/v1.1 headers: Circle-Token: - $secret: - env: CIRCLECI_AUTH_TOKEN + $env: CIRCLECI_AUTH_TOKEN '/jenkins/api': target: http://localhost:8080 headers: Authorization: - $secret: - env: JENKINS_BASIC_AUTH_HEADER + $env: JENKINS_BASIC_AUTH_HEADER '/travisci/api': target: https://api.travis-ci.com changeOrigin: true headers: Authorization: - $secret: - env: TRAVISCI_AUTH_TOKEN + $env: TRAVISCI_AUTH_TOKEN travis-api-version: 3 organization: @@ -52,8 +49,7 @@ sentry: rollbar: organization: spotify accountToken: - $secret: - env: ROLLBAR_ACCOUNT_TOKEN + $env: ROLLBAR_ACCOUNT_TOKEN newrelic: api: @@ -67,174 +63,163 @@ kubernetes: clusterLocatorMethod: 'configMultiTenant' clusters: [] +integrations: + github: + - host: github.com + token: + $env: GITHUB_TOKEN + ### Example for how to add your GitHub Enterprise instance using the API: + # - host: ghe.example.net + # apiBaseUrl: https://ghe.example.net/api/v3 + # token: + # $env: GHE_TOKEN + ### Example for how to add your GitHub Enterprise instance using raw HTTP fetches (token is optional): + # - host: ghe.example.net + # rawBaseUrl: https://ghe.example.net/raw + # token: + # $env: GHE_TOKEN + gitlab: + - host: gitlab.com + token: + $env: GITLAB_TOKEN + bitbucket: + - host: bitbucket.org + username: + $env: BITBUCKET_USERNAME + appPassword: + $env: BITBUCKET_APP_PASSWORD + azure: + - host: dev.azure.com + token: + $env: AZURE_TOKEN + catalog: rules: - allow: [Component, API, Group, User, Template, Location] + processors: - github: - providers: - - target: https://github.com - token: - $secret: - env: GITHUB_PRIVATE_TOKEN - #### Example for how to add your GitHub Enterprise instance using the API: - # - target: https://ghe.example.net - # apiBaseUrl: https://ghe.example.net/api/v3 - # token: - # $secret: - # env: GHE_PRIVATE_TOKEN - #### Example for how to add your GitHub Enterprise instance using raw HTTP fetches (token is optional): - # - target: https://ghe.example.net - # rawBaseUrl: https://ghe.example.net/raw - # token: - # $secret: - # env: GHE_PRIVATE_TOKEN githubOrg: providers: - target: https://github.com token: - $secret: - env: GITHUB_PRIVATE_TOKEN + $env: GITHUB_TOKEN #### Example for how to add your GitHub Enterprise instance using the API: # - target: https://ghe.example.net # apiBaseUrl: https://ghe.example.net/api/v3 # token: - # $secret: - # env: GHE_PRIVATE_TOKEN - bitbucketApi: - username: - $secret: - env: BITBUCKET_USERNAME - appPassword: - $secret: - env: BITBUCKET_APP_PASSWORD - gitlabApi: - privateToken: - $secret: - env: GITLAB_PRIVATE_TOKEN - azureApi: - privateToken: - $secret: - env: AZURE_PRIVATE_TOKEN + # $env: GHE_TOKEN + ldapOrg: + ### Example for how to add your enterprise LDAP server + # providers: + # - target: ldaps://ds.example.net + # bind: + # dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net + # secret: { $secret: { env: LDAP_SECRET } } + # users: + # dn: ou=people,ou=example,dc=example,dc=net + # options: + # filter: (uid=*) + # map: + # description: l + # groups: + # dn: ou=access,ou=groups,ou=example,dc=example,dc=net + # options: + # filter: (&(objectClass=some-group-class)(!(groupType=email))) locations: # Backstage example components - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/all-components.yaml # Example component for github-actions - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/plugins/github-actions/examples/sample.yaml # Example component for techdocs - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/plugins/techdocs-backend/examples/documented-component/documented-component.yaml # Backstage example APIs - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/all-apis.yaml # Backstage example templates - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/all-templates.yaml scaffolder: github: token: - $secret: - env: GITHUB_ACCESS_TOKEN + $env: GITHUB_TOKEN visibility: public # or 'internal' or 'private' gitlab: api: baseUrl: https://gitlab.com token: - $secret: - env: GITLAB_ACCESS_TOKEN + $env: GITLAB_TOKEN azure: baseUrl: https://dev.azure.com/{your-organization} api: token: - $secret: - env: AZURE_PRIVATE_TOKEN + $env: AZURE_TOKEN auth: providers: google: development: clientId: - $secret: - env: AUTH_GOOGLE_CLIENT_ID + $env: AUTH_GOOGLE_CLIENT_ID clientSecret: - $secret: - env: AUTH_GOOGLE_CLIENT_SECRET + $env: AUTH_GOOGLE_CLIENT_SECRET github: development: clientId: - $secret: - env: AUTH_GITHUB_CLIENT_ID + $env: AUTH_GITHUB_CLIENT_ID clientSecret: - $secret: - env: AUTH_GITHUB_CLIENT_SECRET + $env: AUTH_GITHUB_CLIENT_SECRET enterpriseInstanceUrl: - $secret: - env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL + $env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL gitlab: development: clientId: - $secret: - env: AUTH_GITLAB_CLIENT_ID + $env: AUTH_GITLAB_CLIENT_ID clientSecret: - $secret: - env: AUTH_GITLAB_CLIENT_SECRET + $env: AUTH_GITLAB_CLIENT_SECRET audience: - $secret: - env: GITLAB_BASE_URL + $env: GITLAB_BASE_URL saml: entryPoint: 'http://localhost:7001/' issuer: 'passport-saml' okta: development: clientId: - $secret: - env: AUTH_OKTA_CLIENT_ID + $env: AUTH_OKTA_CLIENT_ID clientSecret: - $secret: - env: AUTH_OKTA_CLIENT_SECRET + $env: AUTH_OKTA_CLIENT_SECRET audience: - $secret: - env: AUTH_OKTA_AUDIENCE + $env: AUTH_OKTA_AUDIENCE oauth2: development: clientId: - $secret: - env: AUTH_OAUTH2_CLIENT_ID + $env: AUTH_OAUTH2_CLIENT_ID clientSecret: - $secret: - env: AUTH_OAUTH2_CLIENT_SECRET + $env: AUTH_OAUTH2_CLIENT_SECRET authorizationUrl: - $secret: - env: AUTH_OAUTH2_AUTH_URL + $env: AUTH_OAUTH2_AUTH_URL tokenUrl: - $secret: - env: AUTH_OAUTH2_TOKEN_URL + $env: AUTH_OAUTH2_TOKEN_URL auth0: development: clientId: - $secret: - env: AUTH_AUTH0_CLIENT_ID + $env: AUTH_AUTH0_CLIENT_ID clientSecret: - $secret: - env: AUTH_AUTH0_CLIENT_SECRET + $env: AUTH_AUTH0_CLIENT_SECRET domain: - $secret: - env: AUTH_AUTH0_DOMAIN + $env: AUTH_AUTH0_DOMAIN microsoft: development: clientId: - $secret: - env: AUTH_MICROSOFT_CLIENT_ID + $env: AUTH_MICROSOFT_CLIENT_ID clientSecret: - $secret: - env: AUTH_MICROSOFT_CLIENT_SECRET + $env: AUTH_MICROSOFT_CLIENT_SECRET tenantId: - $secret: - env: AUTH_MICROSOFT_TENANT_ID + $env: AUTH_MICROSOFT_TENANT_ID costInsights: engineerCost: 200000 products: diff --git a/backstage_overview.png b/backstage_overview.png deleted file mode 100644 index 6c939c127c..0000000000 Binary files a/backstage_overview.png and /dev/null differ diff --git a/contrib/docker/kubernetes-example-backend/Dockerfile b/contrib/docker/kubernetes-example-backend/Dockerfile new file mode 100644 index 0000000000..df617decf5 --- /dev/null +++ b/contrib/docker/kubernetes-example-backend/Dockerfile @@ -0,0 +1,35 @@ +FROM node:12-buster + +WORKDIR /usr/src/app + +# (workaround) Install cookiecutter and mkdocs to avoid the need to run docker in docker +RUN cd /tmp && curl -O https://www.python.org/ftp/python/3.8.2/Python-3.8.2.tar.xz && \ + tar -xvf Python-3.8.2.tar.xz && \ + cd Python-3.8.2 && \ + ./configure --enable-optimizations && \ + make -j 4 && \ + make altinstall + +RUN apt update +RUN apt install -y mkdocs + +RUN pip3.8 install mkdocs-techdocs-core + +RUN pip3.8 install cookiecutter && \ + apt remove -y build-essential zlib1g-dev libncurses5-dev libgdbm-dev libnss3-dev libssl-dev libsqlite3-dev libreadline-dev libffi-dev libbz2-dev g++ python-pip python-dev && \ + rm -rf /var/cache/apt/* /tmp/Python-3.8.2 + +# Copy repo skeleton first, to avoid unnecessary docker cache invalidation. +# The skeleton contains the package.json of each package in the monorepo, +# and along with yarn.lock and the root package.json, that's enough to run yarn install. +ADD yarn.lock package.json skeleton.tar ./ + +RUN yarn install --frozen-lockfile --production + +# This will copy the contents of the dist-workspace when running the build-image command. +# Do not use this Dockerfile outside of that command, as it will copy in the source code instead. +COPY . . + +CMD ["node", "packages/backend"] + + diff --git a/contrib/docker/kubernetes-example-backend/README.md b/contrib/docker/kubernetes-example-backend/README.md new file mode 100644 index 0000000000..d0f9d57022 --- /dev/null +++ b/contrib/docker/kubernetes-example-backend/README.md @@ -0,0 +1,13 @@ +# Example backend Dockerfile + +This Dockerfile will build the example backend with certain additional binaries needed to workaround +the docker requirement in the scaffolder and techdocs. + +# Usage + +```bash +yarn docker-build -f --tag +``` + +> The absolute path is necessary as this directory is not copied to the build workspace when building +> the docker image. diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index a9922cfb6c..0000000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,12 +0,0 @@ -# Make sure that before you -# run the docker-compose that you have run -# $ yarn docker-build -version: '3' - -services: - backstage: - image: 'example-backend:latest' - ports: - - '7000:7000' - environment: - NODE_ENV: development diff --git a/docs/assets/architecture-decisions/catalog-core-entities.png b/docs/assets/architecture-decisions/catalog-core-entities.png index b0c7cb4575..b7f238708f 100644 Binary files a/docs/assets/architecture-decisions/catalog-core-entities.png and b/docs/assets/architecture-decisions/catalog-core-entities.png differ diff --git a/docs/assets/architecture-overview/circle-ci.png b/docs/assets/architecture-overview/circle-ci.png index c5695196f1..48aebc9200 100644 Binary files a/docs/assets/architecture-overview/circle-ci.png and b/docs/assets/architecture-overview/circle-ci.png differ diff --git a/docs/assets/architecture-overview/core-vs-plugin-components-highlighted.png b/docs/assets/architecture-overview/core-vs-plugin-components-highlighted.png index 7d08fc5e36..f0a8e8871d 100644 Binary files a/docs/assets/architecture-overview/core-vs-plugin-components-highlighted.png and b/docs/assets/architecture-overview/core-vs-plugin-components-highlighted.png differ diff --git a/docs/assets/architecture-overview/lighthouse-plugin-architecture.png b/docs/assets/architecture-overview/lighthouse-plugin-architecture.png index 0da5d6f042..0d67bcf19b 100644 Binary files a/docs/assets/architecture-overview/lighthouse-plugin-architecture.png and b/docs/assets/architecture-overview/lighthouse-plugin-architecture.png differ diff --git a/docs/assets/architecture-overview/lighthouse-plugin.png b/docs/assets/architecture-overview/lighthouse-plugin.png index 9692fc0833..0b9d0ccc8a 100644 Binary files a/docs/assets/architecture-overview/lighthouse-plugin.png and b/docs/assets/architecture-overview/lighthouse-plugin.png differ diff --git a/docs/assets/architecture-overview/tech-radar-plugin-architecture.png b/docs/assets/architecture-overview/tech-radar-plugin-architecture.png index d55be5ee82..417a2764a1 100644 Binary files a/docs/assets/architecture-overview/tech-radar-plugin-architecture.png and b/docs/assets/architecture-overview/tech-radar-plugin-architecture.png differ diff --git a/docs/assets/architecture-overview/tech-radar-plugin.png b/docs/assets/architecture-overview/tech-radar-plugin.png index dbf39b6c63..062227ac80 100644 Binary files a/docs/assets/architecture-overview/tech-radar-plugin.png and b/docs/assets/architecture-overview/tech-radar-plugin.png differ diff --git a/docs/assets/auth/oauth-popup-flow.svg b/docs/assets/auth/oauth-popup-flow.svg index 2132903783..4a6e76ed21 100644 --- a/docs/assets/auth/oauth-popup-flow.svg +++ b/docs/assets/auth/oauth-popup-flow.svg @@ -1,50 +1,50 @@ OAuth Consent and Refresh FlowBrowserBrowserPopup WindowPopup Windowauth-backend pluginauth-backend pluginConsent ScreenConsent ScreenOAuth ProviderOAuth ProviderComponents on page ask for anaccess token with greaterscope than the existing session.Open popupGET /auth/<provider>/start?scope=some%20scopesRedirect to consent screen withrandom nonce in OAuth state andshort-lived cookie with the same nonce.GET /consent_url?redirect_uri=<redirect_uri>?nonce=<n>where redirect_uri=<app-origin>/auth/<provider>/handler/frameUser consents toaccess the new scope.Redirect to given redirect URL, with authorization codeGET /auth/<provider>/handler/frame?code=<c>&nonce=<n>Request includes the previously set none cookieVerify that the nonce in the cookiematches the nonce in the OAuth stateAuthorization CodeClient IDClient SecretVerify and generate tokensAccess Token(ID Token)(Refresh Token)ScopeExpire TimeSmall HTML page with inlined response payloadStore Refresh Token in HTTP-only cookiepostMessage() with tokens and info or errorClose selfA later point when a refreshis needed. Either because ofa reload or an expiring session.GET /auth/<provider>/tokenRefresh Token cookie includedRefresh TokenClient IDClient SecretAccess Token(ID Token)ScopeExpire TimeTokens and info \ No newline at end of file +--> diff --git a/docs/assets/contributorheader.png b/docs/assets/contributorheader.png index 53d1027d85..64d0cb4cff 100644 Binary files a/docs/assets/contributorheader.png and b/docs/assets/contributorheader.png differ diff --git a/docs/assets/dls/DLS.png b/docs/assets/dls/DLS.png index 94214e4650..f84b94857d 100644 Binary files a/docs/assets/dls/DLS.png and b/docs/assets/dls/DLS.png differ diff --git a/docs/assets/dls/designheader-updated.png b/docs/assets/dls/designheader-updated.png index 56c5a56abb..39aded2252 100644 Binary files a/docs/assets/dls/designheader-updated.png and b/docs/assets/dls/designheader-updated.png differ diff --git a/docs/assets/dls/designheader.png b/docs/assets/dls/designheader.png index e9ace5c2e7..8f160039df 100644 Binary files a/docs/assets/dls/designheader.png and b/docs/assets/dls/designheader.png differ diff --git a/docs/assets/dls/running-storybook.png b/docs/assets/dls/running-storybook.png index 6cf1ded4b9..ce9422b99d 100644 Binary files a/docs/assets/dls/running-storybook.png and b/docs/assets/dls/running-storybook.png differ diff --git a/docs/assets/dls/storybook-page.png b/docs/assets/dls/storybook-page.png index 113f96e589..58470feaff 100644 Binary files a/docs/assets/dls/storybook-page.png and b/docs/assets/dls/storybook-page.png differ diff --git a/docs/assets/getting-started/create-app_output.png b/docs/assets/getting-started/create-app_output.png index caa39ec6d2..875ad05122 100644 Binary files a/docs/assets/getting-started/create-app_output.png and b/docs/assets/getting-started/create-app_output.png differ diff --git a/docs/assets/getting-started/create-plugin_output.png b/docs/assets/getting-started/create-plugin_output.png index f048a9f5fc..47231c2079 100644 Binary files a/docs/assets/getting-started/create-plugin_output.png and b/docs/assets/getting-started/create-plugin_output.png differ diff --git a/docs/assets/headline.png b/docs/assets/headline.png index 83d7b14f21..fef99a269f 100644 Binary files a/docs/assets/headline.png and b/docs/assets/headline.png differ diff --git a/docs/assets/my-plugin_screenshot.png b/docs/assets/my-plugin_screenshot.png index 0b2817fa1e..4f2849a691 100644 Binary files a/docs/assets/my-plugin_screenshot.png and b/docs/assets/my-plugin_screenshot.png differ diff --git a/docs/assets/pop.png b/docs/assets/pop.png index 441126f33c..614136b501 100644 Binary files a/docs/assets/pop.png and b/docs/assets/pop.png differ diff --git a/docs/assets/software-catalog/bsc-edit.png b/docs/assets/software-catalog/bsc-edit.png index d1ac1f52e5..f3a7daa1dd 100644 Binary files a/docs/assets/software-catalog/bsc-edit.png and b/docs/assets/software-catalog/bsc-edit.png differ diff --git a/docs/assets/software-catalog/bsc-register-1.png b/docs/assets/software-catalog/bsc-register-1.png index 117b2ea8ea..0e33c173fb 100644 Binary files a/docs/assets/software-catalog/bsc-register-1.png and b/docs/assets/software-catalog/bsc-register-1.png differ diff --git a/docs/assets/software-catalog/bsc-register-2.png b/docs/assets/software-catalog/bsc-register-2.png index fd1ea7b998..de71141ba0 100644 Binary files a/docs/assets/software-catalog/bsc-register-2.png and b/docs/assets/software-catalog/bsc-register-2.png differ diff --git a/docs/assets/software-catalog/bsc-search.png b/docs/assets/software-catalog/bsc-search.png index 8e417cb076..042e6055e0 100644 Binary files a/docs/assets/software-catalog/bsc-search.png and b/docs/assets/software-catalog/bsc-search.png differ diff --git a/docs/assets/software-catalog/bsc-starred.png b/docs/assets/software-catalog/bsc-starred.png index 27db19c842..c9721c27a2 100644 Binary files a/docs/assets/software-catalog/bsc-starred.png and b/docs/assets/software-catalog/bsc-starred.png differ diff --git a/docs/assets/software-catalog/service-catalog-home.png b/docs/assets/software-catalog/service-catalog-home.png index 742748632e..1d2e8ed317 100644 Binary files a/docs/assets/software-catalog/service-catalog-home.png and b/docs/assets/software-catalog/service-catalog-home.png differ diff --git a/docs/assets/software-catalog/software-model-core-entities.png b/docs/assets/software-catalog/software-model-core-entities.png index b718b7527c..60cb283802 100644 Binary files a/docs/assets/software-catalog/software-model-core-entities.png and b/docs/assets/software-catalog/software-model-core-entities.png differ diff --git a/docs/assets/software-templates/added-to-the-catalog-list.png b/docs/assets/software-templates/added-to-the-catalog-list.png index 4b544e51c1..c73c33ede3 100644 Binary files a/docs/assets/software-templates/added-to-the-catalog-list.png and b/docs/assets/software-templates/added-to-the-catalog-list.png differ diff --git a/docs/assets/software-templates/complete.png b/docs/assets/software-templates/complete.png index eee14fae0e..2a8de0ba1e 100644 Binary files a/docs/assets/software-templates/complete.png and b/docs/assets/software-templates/complete.png differ diff --git a/docs/assets/software-templates/create.png b/docs/assets/software-templates/create.png index 8a2e92b9f4..8123780cd3 100644 Binary files a/docs/assets/software-templates/create.png and b/docs/assets/software-templates/create.png differ diff --git a/docs/assets/software-templates/failed.png b/docs/assets/software-templates/failed.png index bca8c72d6a..4cb7ce84cc 100644 Binary files a/docs/assets/software-templates/failed.png and b/docs/assets/software-templates/failed.png differ diff --git a/docs/assets/software-templates/go-to-catalog.png b/docs/assets/software-templates/go-to-catalog.png index ae16230a02..ea03295846 100644 Binary files a/docs/assets/software-templates/go-to-catalog.png and b/docs/assets/software-templates/go-to-catalog.png differ diff --git a/docs/assets/software-templates/running.png b/docs/assets/software-templates/running.png index 208376e059..681c3702f7 100644 Binary files a/docs/assets/software-templates/running.png and b/docs/assets/software-templates/running.png differ diff --git a/docs/assets/software-templates/template-picked-2.png b/docs/assets/software-templates/template-picked-2.png index 685e356ee8..a1737c8e00 100644 Binary files a/docs/assets/software-templates/template-picked-2.png and b/docs/assets/software-templates/template-picked-2.png differ diff --git a/docs/assets/software-templates/template-picked.png b/docs/assets/software-templates/template-picked.png index 1094acec4a..79b1e260e0 100644 Binary files a/docs/assets/software-templates/template-picked.png and b/docs/assets/software-templates/template-picked.png differ diff --git a/docs/assets/techdocs/documentation-template.png b/docs/assets/techdocs/documentation-template.png index 1f44ad27c6..7539f1f6f4 100644 Binary files a/docs/assets/techdocs/documentation-template.png and b/docs/assets/techdocs/documentation-template.png differ diff --git a/docs/assets/techdocs/techdocs_big_picture.png b/docs/assets/techdocs/techdocs_big_picture.png index ffe437180f..8b8f7a2338 100644 Binary files a/docs/assets/techdocs/techdocs_big_picture.png and b/docs/assets/techdocs/techdocs_big_picture.png differ diff --git a/docs/assets/utility-apis-fig1.svg b/docs/assets/utility-apis-fig1.svg index 47123299c7..3261de88a8 100644 --- a/docs/assets/utility-apis-fig1.svg +++ b/docs/assets/utility-apis-fig1.svg @@ -1,3 +1,3 @@ -
fooApiRef
fooApiRef
FooApi
FooApi
Plugin
Component
Component
App A
App A
App B
App B
DefaultFooApi
DefaultFooApi
CustomFooApi
CustomFooApi
App C
App C
Viewer does not support full SVG 1.1
\ No newline at end of file +
fooApiRef
fooApiRef
FooApi
FooApi
Plugin
Component
Component
App A
App A
App B
App B
DefaultFooApi
DefaultFooApi
CustomFooApi
CustomFooApi
App C
App C
Viewer does not support full SVG 1.1
diff --git a/docs/auth/auth-backend-classes.md b/docs/auth/auth-backend-classes.md index 4ccee8ede9..9dc677b439 100644 --- a/docs/auth/auth-backend-classes.md +++ b/docs/auth/auth-backend-classes.md @@ -67,26 +67,21 @@ auth: google: development: clientId: - $secret: - env: AUTH_GOOGLE_CLIENT_ID + $env: AUTH_GOOGLE_CLIENT_ID clientSecret: - $secret: - env: AUTH_GOOGLE_CLIENT_SECRET + $env: AUTH_GOOGLE_CLIENT_SECRET github: development: clientId: - $secret: - env: AUTH_GITHUB_CLIENT_ID + $env: AUTH_GITHUB_CLIENT_ID clientSecret: - $secret: - env: AUTH_GITHUB_CLIENT_SECRET + $env: AUTH_GITHUB_CLIENT_SECRET enterpriseInstanceUrl: - $secret: - env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL + $env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL gitlab: development: clientId: - $secret: + $env: ... ``` diff --git a/docs/conf/index.md b/docs/conf/index.md index 83eff50a7d..e1dfc30582 100644 --- a/docs/conf/index.md +++ b/docs/conf/index.md @@ -18,7 +18,8 @@ allowing for customization. Configuration is stored in `app-config.yaml` files, with support for suffixes such as `app-config.production.yaml` to override values for specific environments. The configuration files themselves contain plain YAML, but with -support for loading in secrets from various sources using a `$secret` key. +support for loading in secrets from various sources using for example `$env` and +`$file` keys. It is also possible to supply configuration through environment variables, for example `APP_CONFIG_app_baseUrl=https://staging.example.com`. However these diff --git a/docs/conf/writing.md b/docs/conf/writing.md index 6d338e586d..5564e780bb 100644 --- a/docs/conf/writing.md +++ b/docs/conf/writing.md @@ -90,18 +90,17 @@ order: ## Secrets -Secrets are supported via a special `$secret` key, which in turn provides a -number of different ways to read in secrets. To load a configuration value as a -secret, supply an object with a single `$secret` key, and within that supply an -object that describes how the secret is loaded. For example, the following will -read the config key `backend.mySecretKey` from the environment variable -`MY_SECRET_KEY`: +Secrets are supported via a special secret keys that are prefixed with `$`, +which in turn provides a number of different ways to read in secrets. To load a +configuration value as a secret, supply an object with one of the special secret +keys, for example `$env` or `$file`. A full list of supported secret keys can be +found below. For example, the following will read the config key +`backend.mySecretKey` from the environment variable `MY_SECRET_KEY`: ```yaml backend: mySecretKey: - $secret: - env: MY_SECRET_KEY + $env: MY_SECRET_KEY ``` With the above configuration, calling `config.getString('backend.mySecretKey')` @@ -123,8 +122,7 @@ This reads a secret from an environment variable. For example, the following config loads the secret from the `MY_SECRET` env var. ```yaml -$secret: - env: MY_SECRET +$env: MY_SECRET ``` ### File Secrets @@ -135,22 +133,19 @@ following reads the contents of `my-secret.txt` relative to the config file itself: ```yaml -$secret: - file: ./my-secret.txt +$file: ./my-secret.txt ``` ### Data File Secrets This reads secrets from a path within a JSON-like data file. The file path -behaves similar to file secrets, but in addition a `path` is used to point to a -specific value inside the file. Supported file extensions are `.json`, `.yaml`, -and `.yml`. For example, the following would read out `my-secret-key` from -`my-secrets.json`: +behaves similar to file secrets, but with the addition of a url fragment that is +used to point to a specific value inside the file. Supported file extensions are +`.json`, `.yaml`, and `.yml`. For example, the following would read out +`my-secret-key` from `my-secrets.json`: ```yaml -$secret: - data: ./my-secrets.json - path: deployment.key +$data: ./my-secrets.json#deployment.key # my-secrets.json { diff --git a/docs/features/software-catalog/configuration.md b/docs/features/software-catalog/configuration.md index c98bd73ca3..46de2d3d67 100644 --- a/docs/features/software-catalog/configuration.md +++ b/docs/features/software-catalog/configuration.md @@ -11,36 +11,50 @@ tasks, such as reading raw entity data from a remote source, parsing it, transforming it, and validating it. These processors are configured under the `catalog.processors` key. -### Processor: github +### Processor: url -The `github` processor is responsible for fetching entity data from files on -GitHub or GitHub Enterprise. The configuration for this processor lives under -`catalog.processors.github`. Example: +The `url` processor is responsible for fetching entity data from files in any +external provider like GitHub, GitLab, Bitbucket, etc. The configuration of this +processor lives under the top-level `integrations` key, as it is used by other +parts of Backstage too. ```yaml -catalog: - processors: - github: - providers: - - target: https://github.com - token: - $secret: - env: GITHUB_PRIVATE_TOKEN - - target: https://ghe.example.net - apiBaseUrl: https://ghe.example.net/api/v3 - rawBaseUrl: https://ghe.example.net/raw - token: - $secret: - env: GHE_PRIVATE_TOKEN +integrations: + github: + - host: github.com + token: + $env: GITHUB_TOKEN + - host: ghe.example.net + apiBaseUrl: https://ghe.example.net/api/v3 + rawBaseUrl: https://ghe.example.net/raw + token: + $env: GHE_TOKEN + gitlab: + - host: gitlab.com + token: + $env: GITLAB_TOKEN + bitbucket: + - host: bitbucket.org + username: + $env: BITBUCKET_USERNAME + appPassword: + $env: BITBUCKET_APP_PASSWORD + azure: + - host: dev.azure.com + token: + $env: AZURE_TOKEN ``` -The main subkey is `providers`, where you can list the various GitHub compatible -providers you want to be able to fetch data from. Each entry is a structure with -up to four elements: +Each key under `integrations` is a separate configuration for each external +provider. The providers each have their own configuration, so let's look at the +GitHub section as an example. -- `target` (required): The string prefix of the location target that you want to - match on, with no trailing slash. For GitHub, it should be exactly - `https://github.com`. +Directly under the `github` key is a list of provider configurations, where you +can list the various GitHub compatible providers you want to be able to fetch +data from. Each entry is a structure with up to four elements: + +- `host` (optional): The host of the location target that you want to match on. + The default host is `github.com`. - `token` (optional): An authentication token as expected by GitHub. If supplied, it will be passed along with all calls to this provider, both API and raw. If it is not supplied, anonymous access will be used. @@ -74,7 +88,7 @@ the catalog under the `catalog.locations` key, for example: ```yaml catalog: locations: - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml ``` @@ -97,7 +111,7 @@ catalog: - allow: [Component, API, Location, Template] locations: - - type: github + - type: url target: https://github.com/org/example/blob/master/org-data.yaml rules: - allow: [Group] diff --git a/docs/features/software-catalog/extending-the-model.md b/docs/features/software-catalog/extending-the-model.md index 7e2a4c3867..b045660202 100644 --- a/docs/features/software-catalog/extending-the-model.md +++ b/docs/features/software-catalog/extending-the-model.md @@ -16,11 +16,9 @@ Backstage natively supports tracking of the following component ![](../../assets/software-catalog/bsc-extend.png) Since these types are likely not the only kind of software you will want to -track in Backstage, it is possible to - -It is possible to add your own software types that fit your organization's data -model. Inside Spotify our model has grown significantly over the years, and now -includes ML models, Apps, data pipelines and many more. +track in Backstage, it is possible to add your own software types that fit your +organization's data model. Inside Spotify our model has grown significantly over +the years, and now includes ML models, Apps, data pipelines and many more. ## Adding a new type diff --git a/docs/features/software-catalog/index.md b/docs/features/software-catalog/index.md index 8be6a00579..4ba83146db 100644 --- a/docs/features/software-catalog/index.md +++ b/docs/features/software-catalog/index.md @@ -90,7 +90,7 @@ above example can be added using the following configuration: ```yaml catalog: locations: - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml ``` diff --git a/docs/features/software-catalog/installation.md b/docs/features/software-catalog/installation.md index a825020eb6..26990033aa 100644 --- a/docs/features/software-catalog/installation.md +++ b/docs/features/software-catalog/installation.md @@ -166,21 +166,21 @@ our example templates through static configuration. Add the following to the catalog: locations: # Backstage Example Component - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/artist-lookup-component.yaml - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-order-component.yaml - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/podcast-api-component.yaml - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/queue-proxy-component.yaml - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/searcher-component.yaml - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/playback-lib-component.yaml - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/www-artist-component.yaml - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/packages/catalog-model/examples/shuffle-api-component.yaml ``` diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 1bd5528f22..40a4ca3ea1 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -41,27 +41,6 @@ expecting a two-item array out of it. The format of the target part is type-dependent and could conceivably even be an empty string, but the separator colon is always present. -### backstage.io/definition-at-location - -```yaml -# Example -apiVersion: backstage.io/v1alpha1 -kind: API -metadata: - name: petstore - annotations: - backstage.io/definition-at-location: 'url:https://petstore.swagger.io/v2/swagger.json' -spec: - type: openapi -``` - -This annotation allows to fetch an API definition from another location, instead -of wrapping the API definition inside the definition field. This allows to -easily consume existing API definition. The definition is fetched during -ingestion by a processor and included in the entity. It is updated on every -refresh. The annotation contains a location reference string that contains the -location processor type and the target. - ### backstage.io/techdocs-ref ```yaml @@ -179,6 +158,21 @@ fallback (`rollbar.organization` followed by `organization.name`). Specifying this annotation may enable Rollbar related features in Backstage for that entity. +### backstage.io/ldap-rdn, backstage.io/ldap-uuid, backstage.io/ldap-dn + +```yaml +# Example: +metadata: + annotations: + backstage.io/ldap-rdn: my-team + backstage.io/ldap-uuid: c57e8ba2-6cc4-1039-9ebc-d5f241a7ca21 + backstage.io/ldap-dn: cn=my-team,ou=access,ou=groups,ou=spotify,dc=spotify,dc=net +``` + +The value of these annotations are the corresponding attributes that were found +when ingestion the entity from LDAP. Not all of them may be present, depending +on what attributes that the server presented at ingestion time. + ## Deprecated Annotations The following annotations are deprecated, and only listed here to aid in @@ -190,6 +184,25 @@ This annotation was used for a while to enable the GitHub Actions feature. This is now instead using the [github.com/project-slug](#github-com-project-slug) annotation, with the same value format. +### backstage.io/definition-at-location + +This annotation allowed to load the API definition from another location. Now +placeholders can be used instead: + +``` +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: petstore + description: The Petstore API +spec: + type: openapi + lifecycle: production + owner: petstore@example.com + definition: + $text: https://petstore.swagger.io/v2/swagger.json +``` + ## Links - [Descriptor Format: annotations](descriptor-format.md#annotations-optional) diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index 4283af7c72..5a4e0049ab 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -66,7 +66,7 @@ for example ```yaml catalog: locations: - - type: github + - type: url target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml rules: - allow: [Template] diff --git a/docs/features/software-templates/extending/create-your-own-publisher.md b/docs/features/software-templates/extending/create-your-own-publisher.md index ecb097f9d9..546980f033 100644 --- a/docs/features/software-templates/extending/create-your-own-publisher.md +++ b/docs/features/software-templates/extending/create-your-own-publisher.md @@ -34,7 +34,7 @@ import { Octokit } from '@octokit/rest'; import type { PluginEnvironment } from '../types'; export default async function createPlugin({ logger }: PluginEnvironment) { - const githubClient = new Octokit({ auth: process.env.GITHUB_ACCESS_TOKEN }); + const githubClient = new Octokit({ auth: process.env.GITHUB_TOKEN }); const publisher = new GithubPublisher({ client: githubClient }); return await createRouter({ diff --git a/docs/features/software-templates/installation.md b/docs/features/software-templates/installation.md index eff7e53ad3..838c5e2d11 100644 --- a/docs/features/software-templates/installation.md +++ b/docs/features/software-templates/installation.md @@ -191,13 +191,13 @@ our example templates through static configuration. Add the following to the catalog: locations: # Backstage Example Templates - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/springboot-grpc-template/template.yaml - - type: github + - type: url target: https://github.com/spotify/backstage/blob/master/plugins/scaffolder-backend/sample-templates/create-react-app/template.yaml - - type: github + - type: url target: https://github.com/spotify/cookiecutter-golang/blob/master/template.yaml ``` @@ -232,15 +232,13 @@ instance: scaffolder: github: token: - $secret: - env: GITHUB_ACCESS_TOKEN + $env: GITHUB_TOKEN visibility: public # or 'internal' or 'private' gitlab: api: baseUrl: https://gitlab.com token: - $secret: - env: SCAFFOLDER_GITLAB_PRIVATE_TOKEN + $env: GITLAB_TOKEN ``` #### Azure DevOps @@ -257,8 +255,7 @@ scaffolder: baseUrl: https://dev.azure.com/{your-organization} api: token: - $secret: - env: AZURE_PRIVATE_TOKEN + $env: AZURE_TOKEN ``` ### Running the Backend @@ -268,7 +265,7 @@ backend with the new configuration: ```bash cd packages/backend -GITHUB_ACCESS_TOKEN= yarn start +GITHUB_TOKEN= yarn start ``` If you've also set up the frontend plugin, so you should be ready to go browse diff --git a/docs/getting-started/create-an-app.md b/docs/getting-started/create-an-app.md index 831e8af145..9be0e24452 100644 --- a/docs/getting-started/create-an-app.md +++ b/docs/getting-started/create-an-app.md @@ -74,6 +74,7 @@ app. ``` app ├── app-config.yaml +├── catalog-info.yaml ├── lerna.json ├── package.json └── packages @@ -83,6 +84,9 @@ app - **app-config.yaml**: Main configuration file for the app. See [Configuration](https://backstage.io/docs/conf/) for more information. +- **catalog-info.yaml**: Catalog Entities descriptors. See + [Descriptor Format of Catalog Entities](https://backstage.io/docs/features/software-catalog/descriptor-format) + to get started. - **lerna.json**: Contains information about workspaces and other lerna configuration needed for the monorepo setup. - **package.json**: Root package.json for the project. _Note: Be sure that you diff --git a/docs/getting-started/deployment-other.md b/docs/getting-started/deployment-other.md index 2cd9708e2a..ab5833b634 100644 --- a/docs/getting-started/deployment-other.md +++ b/docs/getting-started/deployment-other.md @@ -17,14 +17,3 @@ $ docker run --rm -it -p 7000:7000 -e NODE_ENV=development example-backend:lates ``` Then open http://localhost/ on your browser. - -### Running with `docker-compose` - -There is also a `docker-compose.yaml` that you can use to replace the previous -`docker run` command: - -```bash -$ yarn install -$ yarn docker-build -$ docker-compose up -``` diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/development-environment.md index 154a959f85..aa9aa0bde1 100644 --- a/docs/getting-started/development-environment.md +++ b/docs/getting-started/development-environment.md @@ -88,3 +88,17 @@ yarn create-plugin # Create a new plugin > See > [package.json](https://github.com/spotify/backstage/blob/master/package.json) > for other yarn commands/options. + +## Local configuration + +Backstage allows you to specify the configuration used while running the +application on your computer. Local configuration is read from +`app-config.local.yaml`. This file is ignored by Git, which means that you can +safely use it to reference secrets like GitHub tokens without worrying about +these secrets, inadvertently ending up in the Git repository. You do not need to +copy everything from the default config to the local config. +`app-config.local.yaml` will be merged with `app-config.yaml` and overwrite the +default app configs. + +You can learn more about the local configuration in +[Static Configuration in Backstage](../conf/) section. diff --git a/docs/plugins/existing-plugins.md b/docs/plugins/existing-plugins.md index c1fbda379a..3c1ccb6438 100644 --- a/docs/plugins/existing-plugins.md +++ b/docs/plugins/existing-plugins.md @@ -4,12 +4,15 @@ title: Existing plugins description: Lists of existing open source plugins --- -## Open source plugins +## The Plugin Marketplace -The full list of open source plugins can be found -[here](https://github.com/spotify/backstage/tree/master/plugins). +Open source plugins that you can add to your Backstage deployment can be found +at: -## Plugin gallery +https://backstage.io/plugins -TODO: In the future we would like to have something similar to -https://grafana.com/grafana/plugins +![](https://backstage.io/blog/assets/marketplace.png) + +## Links + +- [[blog] The Plugin Marketplace is open](https://backstage.io/blog/2020/09/30/plugin-marketplace) diff --git a/docs/plugins/proxying.md b/docs/plugins/proxying.md index a691c7f0bc..f55b4d9229 100644 --- a/docs/plugins/proxying.md +++ b/docs/plugins/proxying.md @@ -41,8 +41,7 @@ proxy: target: http://larger.example.com:8080/svc.v1 headers: Authorization: - $secret: - env: EXAMPLE_AUTH_HEADER + $env: EXAMPLE_AUTH_HEADER ``` Each key under the proxy configuration entry is a route to match, below the @@ -58,6 +57,15 @@ is also possible to limit the forwarded HTTP methods with the configuration `allowedMethods`, for example `allowedMethods: ['GET']` to enforce read-only access. +By default, the proxy will only forward safe HTTP request headers to the target. +Those are based on the headers that are considered safe for CORS and includes +headers like `content-type` or `last-modified`, as well as all headers that are +set by the proxy. If the proxy should forward other headers like +`authorization`, this must be enabled by the `allowedHeaders` config, for +example `allowedHeaders: ['Authorization']`. This should help to not +accidentally forward confidential headers (`cookie`, `X-Auth-Request-User`) to +third-parties. + If the value is a string, it is assumed to correspond to: ```yaml diff --git a/docs/tutorials/quickstart-app-auth.md b/docs/tutorials/quickstart-app-auth.md index 7403f2c2dd..8f6b35617b 100644 --- a/docs/tutorials/quickstart-app-auth.md +++ b/docs/tutorials/quickstart-app-auth.md @@ -72,15 +72,12 @@ auth: github: development: clientId: - $secret: - env: AUTH_GITHUB_CLIENT_ID + $env: AUTH_GITHUB_CLIENT_ID clientSecret: - $secret: - env: AUTH_GITHUB_CLIENT_SECRET + $env: AUTH_GITHUB_CLIENT_SECRET ## uncomment the following three lines if using enterprise # enterpriseInstanceUrl: - # $secret: - # env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL + # $env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL ``` 2. Set environment variables in whatever fashion is easiest for you. I chose to diff --git a/lerna.json b/lerna.json index d43d4ff160..7d514abb97 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "packages": ["packages/*", "plugins/*"], "npmClient": "yarn", "useWorkspaces": true, - "version": "0.1.1-alpha.23" + "version": "0.1.1-alpha.24" } diff --git a/microsite/.prettierignore b/microsite/.prettierignore new file mode 100644 index 0000000000..378eac25d3 --- /dev/null +++ b/microsite/.prettierignore @@ -0,0 +1 @@ +build diff --git a/microsite/blog/2020-09-30-plugin-marketplace.md b/microsite/blog/2020-09-30-plugin-marketplace.md index a8b9a88c20..0b96abbc60 100644 --- a/microsite/blog/2020-09-30-plugin-marketplace.md +++ b/microsite/blog/2020-09-30-plugin-marketplace.md @@ -6,7 +6,7 @@ authorURL: https://twitter.com/stalund Backstage has an ambitious goal: to provide engineers with the best possible developer experience. -A fantastic developer experience leads to happy, creative, and productive engineers. Our belief is that engineers should not have to be experts in various infrastructure tools to be productive. Infrastructure should be abstracted away, so that developers can spend more cycles building and testing, quickly and safely. Backstage unifies all your infrastructure tooling, services, and documentation to create a streamlined development environment from end to end. +A great developer experience leads to happy, creative, and productive engineers. Our belief is that engineers should not have to be experts in various infrastructure tools or disciplines (e.g., machine learning or backend) to be productive. Infrastructure should be abstracted away, so that developers can spend more cycles building and testing, quickly and safely. Backstage unifies all your infrastructure tooling, services, and documentation to create a streamlined development environment from end to end. Now you may be thinking, “Yeah, sure, that sounds nice and all, but how does Backstage actually abstract away infrastructure?” The short answer: [plugins](https://backstage.io/plugins). @@ -16,11 +16,11 @@ Now you may be thinking, “Yeah, sure, that sounds nice and all, but how does B Think of plugins as a mini version of an infrastructure tool or service — just the parts you need, made quick and easy. The advantage of using a Backstage plugin instead of a tool’s dedicated UI is that all your infrastructure is packaged into a “single pane of glass” instead of being spread out like independently built “islands”. Once you grow your infrastructure portfolio, the complexity really starts to add up. The Backstage plugin model allows you to add more tools without increasing the cognitive load for your users. -Our goal for the plugin ecosystem is that anything can be a plugin. The definition of a plugin is flexible enough to allow you to expose pretty much any kind of infrastructure or software development tool as a plugin in Backstage. By following strong [design guidelines](https://backstage.io/blog/2020/09/30/backstage-design-system) we ensure the overall user experience stays consistent between plugins. If we all do our job well, the end user of Backstage does not see the boundaries between plugins. They are interacting with one consistent product — with many features. +Our goal for the plugin ecosystem is that anything can be a plugin. The definition of a plugin is flexible enough to allow you to expose pretty much any kind of infrastructure or software development tool as a plugin in Backstage. By following clear [design guidelines](https://backstage.io/blog/2020/09/30/backstage-design-system) we ensure the overall user experience stays consistent between plugins. If we all do our job well, the end user of Backstage does not see the boundaries between plugins. They are interacting with one consistent product — with many features. ## Building an ecosystem -Imagine a not-so-distant future where you walk up to Backstage, install it in your environment, and then choose from a gallery of existing, open source plugins that match whatever infrastructure you use inside your company. That is our vision for the Backstage plugin ecosystem. +Imagine a not-so-distant future where you walk up to Backstage, install it in your environment, and then choose from a gallery of existing, open source plugins that serve and match whatever infrastructure and software development needs you have inside your company. That means you can get started with Backstage and see the gains of improved productivity within days, not months. That is our vision for the Backstage plugin ecosystem. Running services on Kubernetes? There’s a plugin for that. Using Snyk for security scanning? There’s a plugin for that. Grafana, DataDog, or Rollbar for monitoring? There’s a plugin for that. Using Jenkins, CircleCI, GitHub Actions, or Travis CI? Pick the CI plugin of your choice. You get the picture. Regardless of your stack, there’s a plugin that you can use. @@ -32,7 +32,7 @@ This grand vision is actually not that far off. Already today there is a growing ## Creating and suggesting new plugins -Not all plugins you need will be open source. Every company has their own homegrown tooling. Building internal plugins lets you tailor your version of Backstage to be a perfect fit for your infrastructure. If you end up [building plugins](https://backstage.io/docs/plugins/create-a-plugin) that could be useful for other companies, please consider releasing them as open source and [add them to the Marketplace](https://backstage.io/docs/plugins/add-to-marketplace). +Not all plugins you need will be open source. Every company has their own homegrown tooling. Building internal plugins lets you tailor your version of Backstage to be a perfect fit for your infrastructure and software development needs. If you end up [building plugins](https://backstage.io/docs/plugins/create-a-plugin) that could be useful for other companies, please consider releasing them as open source and [add them to the Marketplace](https://backstage.io/docs/plugins/add-to-marketplace). If you start developing a plugin that you aim to release as open source, we suggest that you create a [new plugin Issue](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME). This helps the community know what plugins are in development and opens up opportunities for collaboration. You can also use this process if you have an idea for a good plugin, but you need help building it. @@ -40,4 +40,4 @@ We are really excited to see all the amazing plugins that have already been buil What plugins would you like to see in the Plugin Marketplace? [Tell us](https://github.com/spotify/backstage/issues/new?labels=plugin&template=plugin_template.md&title=%5BPlugin%5D+THE+PLUGIN+NAME)! -_Special shout-out to community member [Iain Billett](https://github.com/iain-b) from Roadie for helping build and contribute the [plugin marketplace page](https://backstage.io/plugins) (as his first PR no less!)._ +_Special shout-out to community member [Iain Billett](https://github.com/iain-b) from [Roadie](https://roadie.io) for helping build and contribute the [Plugin Marketplace page](https://backstage.io/plugins) (as his first PR no less!)._ diff --git a/microsite/blog/assets/2/screen.gif b/microsite/blog/assets/2/screen.gif index a0d52a23b4..f790d639cc 100644 Binary files a/microsite/blog/assets/2/screen.gif and b/microsite/blog/assets/2/screen.gif differ diff --git a/microsite/blog/assets/20-05-20/Service_Catalog_MVP.png b/microsite/blog/assets/20-05-20/Service_Catalog_MVP.png index 4a50f17d15..386b195ea9 100644 Binary files a/microsite/blog/assets/20-05-20/Service_Catalog_MVP.png and b/microsite/blog/assets/20-05-20/Service_Catalog_MVP.png differ diff --git a/microsite/blog/assets/20-05-20/Service_Catalog_MVP_service.png b/microsite/blog/assets/20-05-20/Service_Catalog_MVP_service.png index 8067f665c9..1a48b1900c 100644 Binary files a/microsite/blog/assets/20-05-20/Service_Catalog_MVP_service.png and b/microsite/blog/assets/20-05-20/Service_Catalog_MVP_service.png differ diff --git a/microsite/blog/assets/20-05-20/tabs.png b/microsite/blog/assets/20-05-20/tabs.png index 4a5b11f6fe..71516b6846 100644 Binary files a/microsite/blog/assets/20-05-20/tabs.png and b/microsite/blog/assets/20-05-20/tabs.png differ diff --git a/microsite/blog/assets/20-07-01/auth-landing.png b/microsite/blog/assets/20-07-01/auth-landing.png index e00f4c2e43..064802c90f 100644 Binary files a/microsite/blog/assets/20-07-01/auth-landing.png and b/microsite/blog/assets/20-07-01/auth-landing.png differ diff --git a/microsite/blog/assets/20-07-01/auth-sidebar.png b/microsite/blog/assets/20-07-01/auth-sidebar.png index b089ab6c0a..7c72a96ab5 100644 Binary files a/microsite/blog/assets/20-07-01/auth-sidebar.png and b/microsite/blog/assets/20-07-01/auth-sidebar.png differ diff --git a/microsite/blog/assets/2020-08-05/cards.png b/microsite/blog/assets/2020-08-05/cards.png index 4779618b92..a3e038ce1a 100644 Binary files a/microsite/blog/assets/2020-08-05/cards.png and b/microsite/blog/assets/2020-08-05/cards.png differ diff --git a/microsite/blog/assets/2020-08-05/catalog.png b/microsite/blog/assets/2020-08-05/catalog.png index e9c0c65ade..b1f3680511 100644 Binary files a/microsite/blog/assets/2020-08-05/catalog.png and b/microsite/blog/assets/2020-08-05/catalog.png differ diff --git a/microsite/blog/assets/2020-08-05/create-component.png b/microsite/blog/assets/2020-08-05/create-component.png index 4d815393fc..7f0c630d58 100644 Binary files a/microsite/blog/assets/2020-08-05/create-component.png and b/microsite/blog/assets/2020-08-05/create-component.png differ diff --git a/microsite/blog/assets/2020-08-05/template-form.png b/microsite/blog/assets/2020-08-05/template-form.png index 5805243f59..0643a7f3e8 100644 Binary files a/microsite/blog/assets/2020-08-05/template-form.png and b/microsite/blog/assets/2020-08-05/template-form.png differ diff --git a/microsite/blog/assets/2020-08-05/templates.png b/microsite/blog/assets/2020-08-05/templates.png index e350d463f6..af76121a72 100644 Binary files a/microsite/blog/assets/2020-08-05/templates.png and b/microsite/blog/assets/2020-08-05/templates.png differ diff --git a/microsite/blog/assets/3/audit-list.png b/microsite/blog/assets/3/audit-list.png index 84b64f976a..df18b9923f 100644 Binary files a/microsite/blog/assets/3/audit-list.png and b/microsite/blog/assets/3/audit-list.png differ diff --git a/microsite/blog/assets/3/audit-view.png b/microsite/blog/assets/3/audit-view.png index a2e9716cf4..df5f5691b0 100644 Binary files a/microsite/blog/assets/3/audit-view.png and b/microsite/blog/assets/3/audit-view.png differ diff --git a/microsite/blog/assets/3/create-audit.png b/microsite/blog/assets/3/create-audit.png index cb28de73a4..2b22d66b18 100644 Binary files a/microsite/blog/assets/3/create-audit.png and b/microsite/blog/assets/3/create-audit.png differ diff --git a/microsite/blog/assets/3/lead-copy.png b/microsite/blog/assets/3/lead-copy.png index fbf247eec8..8476461909 100644 Binary files a/microsite/blog/assets/3/lead-copy.png and b/microsite/blog/assets/3/lead-copy.png differ diff --git a/microsite/blog/assets/3/lead.png b/microsite/blog/assets/3/lead.png index 4b60c4961c..639b5b4a9e 100644 Binary files a/microsite/blog/assets/3/lead.png and b/microsite/blog/assets/3/lead.png differ diff --git a/microsite/blog/assets/4/create-app.png b/microsite/blog/assets/4/create-app.png index 52dcc13097..5ff683e5d0 100644 Binary files a/microsite/blog/assets/4/create-app.png and b/microsite/blog/assets/4/create-app.png differ diff --git a/microsite/blog/assets/4/welcome.png b/microsite/blog/assets/4/welcome.png index 5de0d57098..e217fda699 100644 Binary files a/microsite/blog/assets/4/welcome.png and b/microsite/blog/assets/4/welcome.png differ diff --git a/microsite/blog/assets/5/lead.png b/microsite/blog/assets/5/lead.png index 657268fc09..615cf4d349 100644 Binary files a/microsite/blog/assets/5/lead.png and b/microsite/blog/assets/5/lead.png differ diff --git a/microsite/blog/assets/6/header.png b/microsite/blog/assets/6/header.png index 6908e40dbc..3c39971d36 100644 Binary files a/microsite/blog/assets/6/header.png and b/microsite/blog/assets/6/header.png differ diff --git a/microsite/blog/assets/Backstage-mural-DS.png b/microsite/blog/assets/Backstage-mural-DS.png index 2944874706..6e2021a37c 100644 Binary files a/microsite/blog/assets/Backstage-mural-DS.png and b/microsite/blog/assets/Backstage-mural-DS.png differ diff --git a/microsite/blog/assets/announcing-techdocs/discover1.png b/microsite/blog/assets/announcing-techdocs/discover1.png index 5b23e64b43..33869b4d64 100644 Binary files a/microsite/blog/assets/announcing-techdocs/discover1.png and b/microsite/blog/assets/announcing-techdocs/discover1.png differ diff --git a/microsite/blog/assets/announcing-techdocs/discover2.png b/microsite/blog/assets/announcing-techdocs/discover2.png index 3737ee68db..e7ed8d39fe 100644 Binary files a/microsite/blog/assets/announcing-techdocs/discover2.png and b/microsite/blog/assets/announcing-techdocs/discover2.png differ diff --git a/microsite/blog/assets/announcing-techdocs/docs-in-backstage.png b/microsite/blog/assets/announcing-techdocs/docs-in-backstage.png index f3b724ff4f..ee8b4dead0 100644 Binary files a/microsite/blog/assets/announcing-techdocs/docs-in-backstage.png and b/microsite/blog/assets/announcing-techdocs/docs-in-backstage.png differ diff --git a/microsite/blog/assets/announcing-techdocs/feedback-loop1.png b/microsite/blog/assets/announcing-techdocs/feedback-loop1.png index 4e2d0de5df..d78144bded 100644 Binary files a/microsite/blog/assets/announcing-techdocs/feedback-loop1.png and b/microsite/blog/assets/announcing-techdocs/feedback-loop1.png differ diff --git a/microsite/blog/assets/announcing-techdocs/feedback-loop2.png b/microsite/blog/assets/announcing-techdocs/feedback-loop2.png index 516780e3ac..5177451c9e 100644 Binary files a/microsite/blog/assets/announcing-techdocs/feedback-loop2.png and b/microsite/blog/assets/announcing-techdocs/feedback-loop2.png differ diff --git a/microsite/blog/assets/announcing-techdocs/github-issues.png b/microsite/blog/assets/announcing-techdocs/github-issues.png index 3d5f3465c0..dfe345bd0a 100644 Binary files a/microsite/blog/assets/announcing-techdocs/github-issues.png and b/microsite/blog/assets/announcing-techdocs/github-issues.png differ diff --git a/microsite/blog/assets/announcing-techdocs/metrics.png b/microsite/blog/assets/announcing-techdocs/metrics.png index b332a65b0d..58e63951bf 100644 Binary files a/microsite/blog/assets/announcing-techdocs/metrics.png and b/microsite/blog/assets/announcing-techdocs/metrics.png differ diff --git a/microsite/blog/assets/backstage-DS-header.png b/microsite/blog/assets/backstage-DS-header.png index 2160ef3229..2637bcc2a3 100644 Binary files a/microsite/blog/assets/backstage-DS-header.png and b/microsite/blog/assets/backstage-DS-header.png differ diff --git a/microsite/blog/assets/backstage-discord-DS.png b/microsite/blog/assets/backstage-discord-DS.png index a0e513d7d8..bfee45ff18 100644 Binary files a/microsite/blog/assets/backstage-discord-DS.png and b/microsite/blog/assets/backstage-discord-DS.png differ diff --git a/microsite/blog/assets/backstage-figma1-DS.png b/microsite/blog/assets/backstage-figma1-DS.png index 00268eac2a..c2c32a00cb 100644 Binary files a/microsite/blog/assets/backstage-figma1-DS.png and b/microsite/blog/assets/backstage-figma1-DS.png differ diff --git a/microsite/blog/assets/backstage-github-ds.png b/microsite/blog/assets/backstage-github-ds.png index 6bd6bd976e..297056de6b 100644 Binary files a/microsite/blog/assets/backstage-github-ds.png and b/microsite/blog/assets/backstage-github-ds.png differ diff --git a/microsite/blog/assets/backstage-guide-DS.png b/microsite/blog/assets/backstage-guide-DS.png index 8c0c7e7c2e..4d57201010 100644 Binary files a/microsite/blog/assets/backstage-guide-DS.png and b/microsite/blog/assets/backstage-guide-DS.png differ diff --git a/microsite/blog/assets/backstage-guidelines-ds.png b/microsite/blog/assets/backstage-guidelines-ds.png index be5e9ff16f..3176a694db 100644 Binary files a/microsite/blog/assets/backstage-guidelines-ds.png and b/microsite/blog/assets/backstage-guidelines-ds.png differ diff --git a/microsite/blog/assets/backstage-storybook-ds.png b/microsite/blog/assets/backstage-storybook-ds.png index aeff6650eb..94d255b154 100644 Binary files a/microsite/blog/assets/backstage-storybook-ds.png and b/microsite/blog/assets/backstage-storybook-ds.png differ diff --git a/microsite/blog/assets/backstage-world-DS.png b/microsite/blog/assets/backstage-world-DS.png index 18e0da1425..354bf62829 100644 Binary files a/microsite/blog/assets/backstage-world-DS.png and b/microsite/blog/assets/backstage-world-DS.png differ diff --git a/microsite/blog/assets/blog_1.png b/microsite/blog/assets/blog_1.png index f8c3516fa7..7667c54538 100644 Binary files a/microsite/blog/assets/blog_1.png and b/microsite/blog/assets/blog_1.png differ diff --git a/microsite/blog/assets/cncf-sandbox/cncf.png b/microsite/blog/assets/cncf-sandbox/cncf.png index 624094b016..2edfeb9c8e 100644 Binary files a/microsite/blog/assets/cncf-sandbox/cncf.png and b/microsite/blog/assets/cncf-sandbox/cncf.png differ diff --git a/microsite/blog/assets/illustration.svg b/microsite/blog/assets/illustration.svg index 50e865ed4f..bba49c0449 100644 --- a/microsite/blog/assets/illustration.svg +++ b/microsite/blog/assets/illustration.svg @@ -1,104 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/microsite/blog/assets/marketplace.png b/microsite/blog/assets/marketplace.png index 7681b436c7..01132b6aa2 100644 Binary files a/microsite/blog/assets/marketplace.png and b/microsite/blog/assets/marketplace.png differ diff --git a/microsite/package.json b/microsite/package.json index 734f1e4d49..335ada7212 100644 --- a/microsite/package.json +++ b/microsite/package.json @@ -7,13 +7,17 @@ "examples": "docusaurus-examples", "start": "docusaurus-start", "build": "docusaurus-build", + "prettier:check": "prettier --check .", "publish-gh-pages": "docusaurus-publish", "write-translations": "docusaurus-write-translations", "version": "docusaurus-version", "rename-version": "docusaurus-rename-version" }, "devDependencies": { - "docusaurus": "^2.0.0-alpha.64", - "js-yaml": "^3.14.0" - } + "@spotify/prettier-config": "^8.0.0", + "docusaurus": "^2.0.0-alpha.65", + "js-yaml": "^3.14.0", + "prettier": "^2.0.5" + }, + "prettier": "@spotify/prettier-config" } diff --git a/microsite/pages/en/plugins.js b/microsite/pages/en/plugins.js index 0ef8791970..a8255bcd0d 100644 --- a/microsite/pages/en/plugins.js +++ b/microsite/pages/en/plugins.js @@ -29,7 +29,7 @@ const Plugins = () => (
-

Plugin marketplace

+

Plugin Marketplace

Open source plugins that you can add to your Backstage deployment. Learn how to build a plugin. @@ -39,7 +39,7 @@ const Plugins = () => ( className="PluginAddNewButton ButtonFilled" href={addPluginDocsLink} > - Add to marketplace + Add to Marketplace

@@ -93,7 +93,7 @@ const Plugins = () => ( }} > - Add to marketplace + Add to Marketplace

diff --git a/microsite/static/animations/backstage-logos-hero-8.gif b/microsite/static/animations/backstage-logos-hero-8.gif index b9a218f395..2d332d3f38 100644 Binary files a/microsite/static/animations/backstage-logos-hero-8.gif and b/microsite/static/animations/backstage-logos-hero-8.gif differ diff --git a/microsite/static/animations/backstage-plugin-icon-2.gif b/microsite/static/animations/backstage-plugin-icon-2.gif index f8e4107e55..e02a8089f4 100644 Binary files a/microsite/static/animations/backstage-plugin-icon-2.gif and b/microsite/static/animations/backstage-plugin-icon-2.gif differ diff --git a/microsite/static/animations/backstage-service-catalog-icon-1.gif b/microsite/static/animations/backstage-service-catalog-icon-1.gif index e888a46e90..72414a8a5d 100644 Binary files a/microsite/static/animations/backstage-service-catalog-icon-1.gif and b/microsite/static/animations/backstage-service-catalog-icon-1.gif differ diff --git a/microsite/static/animations/backstage-software-templates-icon-5.gif b/microsite/static/animations/backstage-software-templates-icon-5.gif index 8e885393de..a5332ab3c8 100644 Binary files a/microsite/static/animations/backstage-software-templates-icon-5.gif and b/microsite/static/animations/backstage-software-templates-icon-5.gif differ diff --git a/microsite/static/animations/backstage-speed-paradox-7.gif b/microsite/static/animations/backstage-speed-paradox-7.gif index 3112e7698f..b6f16d2394 100644 Binary files a/microsite/static/animations/backstage-speed-paradox-7.gif and b/microsite/static/animations/backstage-speed-paradox-7.gif differ diff --git a/microsite/static/animations/backstage-standards-paradox-4.gif b/microsite/static/animations/backstage-standards-paradox-4.gif index 9cd921cce2..775cc38336 100644 Binary files a/microsite/static/animations/backstage-standards-paradox-4.gif and b/microsite/static/animations/backstage-standards-paradox-4.gif differ diff --git a/microsite/static/animations/backstage-techdocs-icon-1.gif b/microsite/static/animations/backstage-techdocs-icon-1.gif index 40f4ca4dd6..72aba32b09 100644 Binary files a/microsite/static/animations/backstage-techdocs-icon-1.gif and b/microsite/static/animations/backstage-techdocs-icon-1.gif differ diff --git a/microsite/static/css/plugins.css b/microsite/static/css/plugins.css index a753e16047..9a8bbafa46 100644 --- a/microsite/static/css/plugins.css +++ b/microsite/static/css/plugins.css @@ -1,5 +1,5 @@ .PluginCard { - background-color: #272822; + background-color: #282828; height: 100%; padding: 16px; display: flex; @@ -53,13 +53,12 @@ .ButtonFilled { padding: 4px 8px; border-radius: 4px; - background-color: #36baa2; - color: white; + color: #69ddc7; margin-top: 36px; } .ButtonFilled:hover { - border: 1px solid #36baa2; + border: 1px solid #69ddc7; background-color: transparent; } @@ -67,8 +66,8 @@ font-size: small; border-radius: 16px; padding: 2px 8px; - border: 1px solid #36baa2; - color: #36baa2; + border: 1px solid #69ddc7; + color: #69ddc7; } .PluginCardLink { @@ -112,5 +111,5 @@ } #add-plugin-card { - border: 1px solid #36baa2; + border: 1px solid #69ddc7; } diff --git a/microsite/static/img/android-chrome-192x192.png b/microsite/static/img/android-chrome-192x192.png index 4660f988c1..6b1af9bd95 100644 Binary files a/microsite/static/img/android-chrome-192x192.png and b/microsite/static/img/android-chrome-192x192.png differ diff --git a/microsite/static/img/android-chrome-512x512.png b/microsite/static/img/android-chrome-512x512.png index d9b0a6ba78..736ff90f8b 100644 Binary files a/microsite/static/img/android-chrome-512x512.png and b/microsite/static/img/android-chrome-512x512.png differ diff --git a/microsite/static/img/apple-touch-icon.png b/microsite/static/img/apple-touch-icon.png index 57c05cfc9a..c1dcaad2a8 100644 Binary files a/microsite/static/img/apple-touch-icon.png and b/microsite/static/img/apple-touch-icon.png differ diff --git a/microsite/static/img/backstage-logo-cncf.svg b/microsite/static/img/backstage-logo-cncf.svg index b7d10e429c..b5ff591d1b 100644 --- a/microsite/static/img/backstage-logo-cncf.svg +++ b/microsite/static/img/backstage-logo-cncf.svg @@ -1 +1 @@ -05 Logo_Black \ No newline at end of file +05 Logo_Black \ No newline at end of file diff --git a/microsite/static/img/cards-mobile.png b/microsite/static/img/cards-mobile.png index 9cb74e0aff..780da80e3b 100644 Binary files a/microsite/static/img/cards-mobile.png and b/microsite/static/img/cards-mobile.png differ diff --git a/microsite/static/img/cards-wide.png b/microsite/static/img/cards-wide.png index db72cc072b..392bd38382 100644 Binary files a/microsite/static/img/cards-wide.png and b/microsite/static/img/cards-wide.png differ diff --git a/microsite/static/img/cards.png b/microsite/static/img/cards.png index f388dfdb4c..b0f843e29c 100644 Binary files a/microsite/static/img/cards.png and b/microsite/static/img/cards.png differ diff --git a/microsite/static/img/cncf-color.svg b/microsite/static/img/cncf-color.svg index 12f7d3e48a..2b9d5117f9 100644 --- a/microsite/static/img/cncf-color.svg +++ b/microsite/static/img/cncf-color.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/microsite/static/img/cncf-white.svg b/microsite/static/img/cncf-white.svg index d94aaf3249..c5e3b2af9a 100644 --- a/microsite/static/img/cncf-white.svg +++ b/microsite/static/img/cncf-white.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/microsite/static/img/code.gif b/microsite/static/img/code.gif index c0d510f0cc..f5031ed50c 100644 Binary files a/microsite/static/img/code.gif and b/microsite/static/img/code.gif differ diff --git a/microsite/static/img/code.png b/microsite/static/img/code.png index b9d53ee4c5..5e33734b5f 100644 Binary files a/microsite/static/img/code.png and b/microsite/static/img/code.png differ diff --git a/microsite/static/img/compliance.svg b/microsite/static/img/compliance.svg index 7214c5bd9e..b4e72def73 100644 --- a/microsite/static/img/compliance.svg +++ b/microsite/static/img/compliance.svg @@ -1,68 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/microsite/static/img/components-with-filter-small.png b/microsite/static/img/components-with-filter-small.png index 4cbe2e8371..bf80faaafb 100644 Binary files a/microsite/static/img/components-with-filter-small.png and b/microsite/static/img/components-with-filter-small.png differ diff --git a/microsite/static/img/components-with-filter.png b/microsite/static/img/components-with-filter.png index 294e078cf2..a232699fa4 100644 Binary files a/microsite/static/img/components-with-filter.png and b/microsite/static/img/components-with-filter.png differ diff --git a/microsite/static/img/demo-illustration.svg b/microsite/static/img/demo-illustration.svg index 984b97335b..036912cafa 100644 --- a/microsite/static/img/demo-illustration.svg +++ b/microsite/static/img/demo-illustration.svg @@ -1,66 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/microsite/static/img/developers.svg b/microsite/static/img/developers.svg index f761828ec7..0c15235213 100644 --- a/microsite/static/img/developers.svg +++ b/microsite/static/img/developers.svg @@ -1,49 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/microsite/static/img/docs-like-code.png b/microsite/static/img/docs-like-code.png index c9fbd0c466..0913ee1cf4 100644 Binary files a/microsite/static/img/docs-like-code.png and b/microsite/static/img/docs-like-code.png differ diff --git a/microsite/static/img/dot.svg b/microsite/static/img/dot.svg index 93c7f25bdd..7090c946f7 100644 --- a/microsite/static/img/dot.svg +++ b/microsite/static/img/dot.svg @@ -1,6146 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/microsite/static/img/embraced.svg b/microsite/static/img/embraced.svg index f1c023eea6..e0bacf2a9b 100644 --- a/microsite/static/img/embraced.svg +++ b/microsite/static/img/embraced.svg @@ -1,94 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/microsite/static/img/favicon.svg b/microsite/static/img/favicon.svg index 535eea5543..8422122718 100644 --- a/microsite/static/img/favicon.svg +++ b/microsite/static/img/favicon.svg @@ -1,17 +1 @@ - - - favicon - - - - - +favicon \ No newline at end of file diff --git a/microsite/static/img/laptop-screen.svg b/microsite/static/img/laptop-screen.svg index a35f880a22..6e63cb50a1 100644 --- a/microsite/static/img/laptop-screen.svg +++ b/microsite/static/img/laptop-screen.svg @@ -1,9 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/microsite/static/img/laptop.svg b/microsite/static/img/laptop.svg index c3aad71ce9..5a4b97d216 100644 --- a/microsite/static/img/laptop.svg +++ b/microsite/static/img/laptop.svg @@ -1,45 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/microsite/static/img/logo-black-248x250.png b/microsite/static/img/logo-black-248x250.png index fdf9a00311..a9cb12ca8e 100644 Binary files a/microsite/static/img/logo-black-248x250.png and b/microsite/static/img/logo-black-248x250.png differ diff --git a/microsite/static/img/logo-gradient-on-dark.svg b/microsite/static/img/logo-gradient-on-dark.svg index 57bb1bdd24..2cd4fad4e5 100644 --- a/microsite/static/img/logo-gradient-on-dark.svg +++ b/microsite/static/img/logo-gradient-on-dark.svg @@ -1,16 +1 @@ - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/microsite/static/img/logo.svg b/microsite/static/img/logo.svg index b3babaa39a..bee663d69e 100644 --- a/microsite/static/img/logo.svg +++ b/microsite/static/img/logo.svg @@ -1 +1 @@ -03 Logo_Teal \ No newline at end of file +03 Logo_Teal \ No newline at end of file diff --git a/microsite/static/img/logos-background.svg b/microsite/static/img/logos-background.svg index 141b9428c6..610085b9c0 100644 --- a/microsite/static/img/logos-background.svg +++ b/microsite/static/img/logos-background.svg @@ -1,19 +1 @@ - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/microsite/static/img/logos-signature.svg b/microsite/static/img/logos-signature.svg index 5acbac2a8c..2a10e8f3b4 100644 --- a/microsite/static/img/logos-signature.svg +++ b/microsite/static/img/logos-signature.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/microsite/static/img/logos.svg b/microsite/static/img/logos.svg index d2e0f16c3d..5e9163ee3a 100644 --- a/microsite/static/img/logos.svg +++ b/microsite/static/img/logos.svg @@ -1,142 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/microsite/static/img/mstile-150x150.png b/microsite/static/img/mstile-150x150.png index 6a63de8a44..713e7eb495 100644 Binary files a/microsite/static/img/mstile-150x150.png and b/microsite/static/img/mstile-150x150.png differ diff --git a/microsite/static/img/open-platform.svg b/microsite/static/img/open-platform.svg index 68b35afbec..6abbc0c7d9 100644 --- a/microsite/static/img/open-platform.svg +++ b/microsite/static/img/open-platform.svg @@ -1,54 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/microsite/static/img/photo-montage.svg b/microsite/static/img/photo-montage.svg index bb7142b19a..88c1809d2d 100644 --- a/microsite/static/img/photo-montage.svg +++ b/microsite/static/img/photo-montage.svg @@ -1,35 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/microsite/static/img/plugin-mobile.svg b/microsite/static/img/plugin-mobile.svg index 503e075cea..3d8db71d02 100644 --- a/microsite/static/img/plugin-mobile.svg +++ b/microsite/static/img/plugin-mobile.svg @@ -1,104 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/microsite/static/img/plugin.svg b/microsite/static/img/plugin.svg index 6ac64e3059..7885f414a7 100644 --- a/microsite/static/img/plugin.svg +++ b/microsite/static/img/plugin.svg @@ -1,108 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/microsite/static/img/screen.gif b/microsite/static/img/screen.gif index ecc82d0875..f790d639cc 100644 Binary files a/microsite/static/img/screen.gif and b/microsite/static/img/screen.gif differ diff --git a/microsite/static/img/spring-boot-service.png b/microsite/static/img/spring-boot-service.png index fccea79822..da0122abb1 100644 Binary files a/microsite/static/img/spring-boot-service.png and b/microsite/static/img/spring-boot-service.png differ diff --git a/microsite/static/img/techdocs-static-mobile.png b/microsite/static/img/techdocs-static-mobile.png index 082518fe21..c827b49a83 100644 Binary files a/microsite/static/img/techdocs-static-mobile.png and b/microsite/static/img/techdocs-static-mobile.png differ diff --git a/microsite/static/img/techdocs-web.png b/microsite/static/img/techdocs-web.png index d1306fb6e7..c460e8eb4d 100644 Binary files a/microsite/static/img/techdocs-web.png and b/microsite/static/img/techdocs-web.png differ diff --git a/microsite/static/img/techdocs.gif b/microsite/static/img/techdocs.gif index 1f0b2b034d..b133caf058 100644 Binary files a/microsite/static/img/techdocs.gif and b/microsite/static/img/techdocs.gif differ diff --git a/microsite/static/img/techdocs.png b/microsite/static/img/techdocs.png index a726012b68..f789745582 100644 Binary files a/microsite/static/img/techdocs.png and b/microsite/static/img/techdocs.png differ diff --git a/microsite/static/img/techdocs2.gif b/microsite/static/img/techdocs2.gif index ba937581b3..201f192330 100644 Binary files a/microsite/static/img/techdocs2.gif and b/microsite/static/img/techdocs2.gif differ diff --git a/microsite/static/img/techs.png b/microsite/static/img/techs.png index 20026ccc92..46dfad36db 100644 Binary files a/microsite/static/img/techs.png and b/microsite/static/img/techs.png differ diff --git a/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_05 Logo_Black.jpg b/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_05 Logo_Black.jpg index cb0b6c7959..93b69e963c 100644 Binary files a/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_05 Logo_Black.jpg and b/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_05 Logo_Black.jpg differ diff --git a/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.jpg b/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.jpg index cbd97e78ea..76f691ba5e 100644 Binary files a/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.jpg and b/microsite/static/logo_assets/jpeg/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.jpg differ diff --git a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_01_Logo_White.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_01_Logo_White.png index 84a1dba855..02db1a275c 100644 Binary files a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_01_Logo_White.png and b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_01_Logo_White.png differ diff --git a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_02_Icon_White.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_02_Icon_White.png index 29264f8ac0..0820c6ae50 100644 Binary files a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_02_Icon_White.png and b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_02_Icon_White.png differ diff --git a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_03_Logo_Teal.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_03_Logo_Teal.png index 092599ce63..b75137ed31 100644 Binary files a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_03_Logo_Teal.png and b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_03_Logo_Teal.png differ diff --git a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png index d662301101..a9c69443ff 100644 Binary files a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png and b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_04_Icon_Teal.png differ diff --git a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_05_Logo_Black.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_05_Logo_Black.png index 7da1808784..dfc99e57ed 100644 Binary files a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_05_Logo_Black.png and b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_05_Logo_Black.png differ diff --git a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.png index 5049d54513..b33b97254c 100644 Binary files a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.png and b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_06_Icon_Black.png differ diff --git a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_07_Large_Icon_Gradient.png b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_07_Large_Icon_Gradient.png index 889017fbfb..8a21bc14ae 100644 Binary files a/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_07_Large_Icon_Gradient.png and b/microsite/static/logo_assets/png/Backstage_Identity_Assets_Artwork_RGB_07_Large_Icon_Gradient.png differ diff --git a/microsite/static/logo_assets/svg/Icon_Black.svg b/microsite/static/logo_assets/svg/Icon_Black.svg index 2eb2dda646..bcd35e6648 100644 --- a/microsite/static/logo_assets/svg/Icon_Black.svg +++ b/microsite/static/logo_assets/svg/Icon_Black.svg @@ -1 +1 @@ -06 Icon_Black \ No newline at end of file +06 Icon_Black \ No newline at end of file diff --git a/microsite/static/logo_assets/svg/Icon_Gradient.svg b/microsite/static/logo_assets/svg/Icon_Gradient.svg index bbdb4bba27..2f361046e7 100644 --- a/microsite/static/logo_assets/svg/Icon_Gradient.svg +++ b/microsite/static/logo_assets/svg/Icon_Gradient.svg @@ -1 +1 @@ -07 Large Icon_Gradient \ No newline at end of file +07 Large Icon_Gradient \ No newline at end of file diff --git a/microsite/static/logo_assets/svg/Icon_Teal.svg b/microsite/static/logo_assets/svg/Icon_Teal.svg index 7152749073..eaad1cf2d3 100644 --- a/microsite/static/logo_assets/svg/Icon_Teal.svg +++ b/microsite/static/logo_assets/svg/Icon_Teal.svg @@ -1 +1 @@ -04 Icon_Teal \ No newline at end of file +04 Icon_Teal \ No newline at end of file diff --git a/microsite/static/logo_assets/svg/Icon_White.svg b/microsite/static/logo_assets/svg/Icon_White.svg index c1d6f388b4..84e3c546a5 100644 --- a/microsite/static/logo_assets/svg/Icon_White.svg +++ b/microsite/static/logo_assets/svg/Icon_White.svg @@ -1 +1 @@ -02 Icon_White \ No newline at end of file +02 Icon_White \ No newline at end of file diff --git a/microsite/static/logo_assets/svg/Logo_Black.svg b/microsite/static/logo_assets/svg/Logo_Black.svg index b7d10e429c..b5ff591d1b 100644 --- a/microsite/static/logo_assets/svg/Logo_Black.svg +++ b/microsite/static/logo_assets/svg/Logo_Black.svg @@ -1 +1 @@ -05 Logo_Black \ No newline at end of file +05 Logo_Black \ No newline at end of file diff --git a/microsite/static/logo_assets/svg/Logo_Teal.svg b/microsite/static/logo_assets/svg/Logo_Teal.svg index b3babaa39a..bee663d69e 100644 --- a/microsite/static/logo_assets/svg/Logo_Teal.svg +++ b/microsite/static/logo_assets/svg/Logo_Teal.svg @@ -1 +1 @@ -03 Logo_Teal \ No newline at end of file +03 Logo_Teal \ No newline at end of file diff --git a/microsite/static/logo_assets/svg/Logo_White.svg b/microsite/static/logo_assets/svg/Logo_White.svg index 7b9c543951..f09a8fdc6e 100644 --- a/microsite/static/logo_assets/svg/Logo_White.svg +++ b/microsite/static/logo_assets/svg/Logo_White.svg @@ -1 +1 @@ -01 Logo_White \ No newline at end of file +01 Logo_White \ No newline at end of file diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 84b5d50d60..40e46c3545 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -959,6 +959,11 @@ resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd" integrity sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow== +"@spotify/prettier-config@^8.0.0": + version "8.0.0" + resolved "https://registry.npmjs.org/@spotify/prettier-config/-/prettier-config-8.0.0.tgz#8b6c2bd579ddc54887155a0721fe04e96c89f7f2" + integrity sha512-so8w32ZV42CHWxOEXcBtbNO/hLXFrQNXVmhfzhUI6dVB9cq2xjRaiqu8GjFj8LvKbWpPj+S+KwTIS4aDVWqrFQ== + "@types/cheerio@^0.22.8": version "0.22.21" resolved "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.21.tgz#5e37887de309ba11b2e19a6e14cad7874b31a8a3" @@ -2228,10 +2233,10 @@ dir-glob@2.0.0: arrify "^1.0.1" path-type "^3.0.0" -docusaurus@^2.0.0-alpha.64: - version "2.0.0-alpha.64" - resolved "https://registry.yarnpkg.com/docusaurus/-/docusaurus-2.0.0-alpha.64.tgz#7833960e9d338403894a27b79058aa4076a676d3" - integrity sha512-ARCx0GwAvc5qx7AHvRVZidZuoDTfaaGXzgmkU23NahU6jzO/aK2Q1bH8IKNEQ5C2JuDerQ/hHDh80N20ijk82g== +docusaurus@^2.0.0-alpha.65: + version "2.0.0-alpha.65" + resolved "https://registry.yarnpkg.com/docusaurus/-/docusaurus-2.0.0-alpha.65.tgz#e2b84985529deb79797aff52aa39ffbe03cd8a20" + integrity sha512-OEnKbXLPy3EpbQA/Dj0kakB6fg/Qni1LORTIero+5Q5G0QT5OH4FAbBQOy3L1qQH0S5Bv18zESxX9Nvclg20ag== dependencies: "@babel/core" "^7.9.0" "@babel/plugin-proposal-class-properties" "^7.8.3" @@ -5199,6 +5204,11 @@ prepend-http@^2.0.0: resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= +prettier@^2.0.5: + version "2.1.2" + resolved "https://registry.npmjs.org/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" + integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== + prismjs@^1.17.1: version "1.21.0" resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.21.0.tgz#36c086ec36b45319ec4218ee164c110f9fc015a3" diff --git a/package.json b/package.json index b9c84a4d90..371646074c 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ }, "version": "1.0.0", "devDependencies": { - "@changesets/cli": "2.10.2", + "@changesets/cli": "^2.11.0", "@spotify/eslint-config-oss": "^1.0.1", "@spotify/prettier-config": "^8.0.0", "concurrently": "^5.2.0", diff --git a/packages/app/package.json b/packages/app/package.json index 2373887cc6..a3b150cef6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,40 +1,42 @@ { "name": "example-app", - "version": "0.1.1-alpha.23", + "version": "0.1.1-alpha.24", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.23", - "@backstage/cli": "^0.1.1-alpha.23", - "@backstage/core": "^0.1.1-alpha.23", - "@backstage/plugin-api-docs": "^0.1.1-alpha.23", - "@backstage/plugin-catalog": "^0.1.1-alpha.23", - "@backstage/plugin-circleci": "^0.1.1-alpha.23", - "@backstage/plugin-cloudbuild": "^0.1.1-alpha.23", - "@backstage/plugin-cost-insights": "^0.1.1-alpha.23", - "@backstage/plugin-explore": "^0.1.1-alpha.23", - "@backstage/plugin-gcp-projects": "^0.1.1-alpha.23", - "@backstage/plugin-github-actions": "^0.1.1-alpha.23", - "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.23", - "@backstage/plugin-graphiql": "^0.1.1-alpha.23", - "@backstage/plugin-jenkins": "^0.1.1-alpha.23", - "@backstage/plugin-kubernetes": "^0.1.1-alpha.23", - "@backstage/plugin-lighthouse": "^0.1.1-alpha.23", - "@backstage/plugin-newrelic": "^0.1.1-alpha.23", - "@backstage/plugin-register-component": "^0.1.1-alpha.23", - "@backstage/plugin-rollbar": "^0.1.1-alpha.23", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.23", - "@backstage/plugin-sentry": "^0.1.1-alpha.23", - "@backstage/plugin-tech-radar": "^0.1.1-alpha.23", - "@backstage/plugin-techdocs": "^0.1.1-alpha.23", - "@backstage/plugin-welcome": "^0.1.1-alpha.23", - "@backstage/test-utils": "^0.1.1-alpha.23", - "@backstage/theme": "^0.1.1-alpha.23", + "@backstage/catalog-model": "^0.1.1-alpha.24", + "@backstage/cli": "^0.1.1-alpha.24", + "@backstage/core": "^0.1.1-alpha.24", + "@backstage/plugin-api-docs": "^0.1.1-alpha.24", + "@backstage/plugin-catalog": "^0.1.1-alpha.24", + "@backstage/plugin-circleci": "^0.1.1-alpha.24", + "@backstage/plugin-cloudbuild": "^0.1.1-alpha.24", + "@backstage/plugin-cost-insights": "^0.1.1-alpha.24", + "@backstage/plugin-explore": "^0.1.1-alpha.24", + "@backstage/plugin-gcp-projects": "^0.1.1-alpha.24", + "@backstage/plugin-github-actions": "^0.1.1-alpha.24", + "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.24", + "@backstage/plugin-graphiql": "^0.1.1-alpha.24", + "@backstage/plugin-jenkins": "^0.1.1-alpha.24", + "@backstage/plugin-kubernetes": "^0.1.1-alpha.24", + "@backstage/plugin-lighthouse": "^0.1.1-alpha.24", + "@backstage/plugin-newrelic": "^0.1.1-alpha.24", + "@backstage/plugin-register-component": "^0.1.1-alpha.24", + "@backstage/plugin-rollbar": "^0.1.1-alpha.24", + "@backstage/plugin-scaffolder": "^0.1.1-alpha.24", + "@backstage/plugin-sentry": "^0.1.1-alpha.24", + "@backstage/plugin-tech-radar": "^0.1.1-alpha.24", + "@backstage/plugin-techdocs": "^0.1.1-alpha.24", + "@backstage/plugin-user-settings": "^0.1.1-alpha.24", + "@backstage/plugin-welcome": "^0.1.1-alpha.24", + "@backstage/test-utils": "^0.1.1-alpha.24", + "@backstage/theme": "^0.1.1-alpha.24", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@octokit/rest": "^18.0.0", - "@roadiehq/backstage-plugin-github-pull-requests": "0.3.0", + "@roadiehq/backstage-plugin-github-pull-requests": "^0.4.3", "@roadiehq/backstage-plugin-travis-ci": "^0.2.3", + "dayjs": "^1.9.1", "history": "^5.0.0", "prop-types": "^15.7.2", "react": "^16.12.0", @@ -43,6 +45,7 @@ "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3", + "regression": "^2.0.1", "zen-observable": "^0.8.15" }, "devDependencies": { @@ -54,6 +57,7 @@ "@types/jquery": "^3.3.34", "@types/node": "^12.0.0", "@types/react-dom": "^16.9.8", + "@types/regression": "^2.0.0", "@types/zen-observable": "^0.8.0", "cross-env": "^7.0.0", "cypress": "^4.2.0", diff --git a/packages/app/public/android-chrome-192x192.png b/packages/app/public/android-chrome-192x192.png index 4660f988c1..eec0ae25b9 100644 Binary files a/packages/app/public/android-chrome-192x192.png and b/packages/app/public/android-chrome-192x192.png differ diff --git a/packages/app/public/android-chrome-512x512.png b/packages/app/public/android-chrome-512x512.png index d9b0a6ba78..ac0578133b 100644 Binary files a/packages/app/public/android-chrome-512x512.png and b/packages/app/public/android-chrome-512x512.png differ diff --git a/packages/app/public/apple-touch-icon.png b/packages/app/public/apple-touch-icon.png index 57c05cfc9a..6a45afb38f 100644 Binary files a/packages/app/public/apple-touch-icon.png and b/packages/app/public/apple-touch-icon.png differ diff --git a/packages/app/public/favicon.svg b/packages/app/public/favicon.svg index 351dcc8809..7f2d375cd8 100644 --- a/packages/app/public/favicon.svg +++ b/packages/app/public/favicon.svg @@ -1,17 +1 @@ - - - Backstage favicon - - - - - +Backstage favicon \ No newline at end of file diff --git a/packages/app/public/safari-pinned-tab.svg b/packages/app/public/safari-pinned-tab.svg index 3b0f666390..0f500b3002 100644 --- a/packages/app/public/safari-pinned-tab.svg +++ b/packages/app/public/safari-pinned-tab.svg @@ -1,28 +1 @@ - - - - -Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - - - - +Created by potrace 1.11, written by Peter Selinger 2001-2013 \ No newline at end of file diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 667b14366f..81d08f58a9 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -33,6 +33,7 @@ import { Router as GraphiQLRouter } from '@backstage/plugin-graphiql'; import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar'; import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse'; import { Router as RegisterComponentRouter } from '@backstage/plugin-register-component'; +import { Router as SettingsRouter } from '@backstage/plugin-user-settings'; import { Route, Routes, Navigate } from 'react-router'; import { EntityPage } from './components/catalog/EntityPage'; @@ -81,6 +82,7 @@ const AppRoutes = () => ( path="/register-component" element={} /> + } /> {...deprecatedAppRoutes} ); diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 45ffca7801..e947e5c91d 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -35,11 +35,10 @@ import { SidebarDivider, SidebarSearchField, SidebarSpace, - SidebarUserSettings, - DefaultProviderSettings, } from '@backstage/core'; import { NavLink } from 'react-router-dom'; import { graphiQLRouteRef } from '@backstage/plugin-graphiql'; +import { Settings as SidebarSettings } from '@backstage/plugin-user-settings'; const useSidebarLogoStyles = makeStyles({ root: { @@ -103,7 +102,7 @@ const Root: FC<{}> = ({ children }) => ( /> - } /> + {children} diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 0c7dfd926e..393ffe2631 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -47,8 +47,13 @@ import { useEntity, } from '@backstage/plugin-catalog'; import { Entity } from '@backstage/catalog-model'; -import { Grid } from '@material-ui/core'; -import { WarningPanel } from '@backstage/core'; +import { Button, Grid } from '@material-ui/core'; +import { EmptyState } from '@backstage/core'; +import { + EmbeddedRouter as LighthouseRouter, + LastLighthouseAuditCard, + isPluginApplicableToEntity as isLighthouseAvailable, +} from '@backstage/plugin-lighthouse/'; const CICDSwitcher = ({ entity }: { entity: Entity }) => { // This component is just an example of how you can implement your company's logic in entity page. @@ -66,10 +71,20 @@ const CICDSwitcher = ({ entity }: { entity: Entity }) => { return ; default: return ( - - No CI/CD is available for this entity. Check corresponding - annotations! - + + Read more + + } + /> ); } }; @@ -105,6 +120,11 @@ const OverviewContent = ({ entity }: { entity: Entity }) => ( + {isLighthouseAvailable(entity) && ( + + + + )} ); @@ -155,6 +175,11 @@ const WebsiteEntityPage = ({ entity }: { entity: Entity }) => ( title="CI/CD" element={} /> + } + /> P\d+[DM])\//); + const { duration } = match!.groups!; + return duration as Duration; +} + +function aggregationFor( + duration: Duration, + baseline: number, +): DateAggregation[] { + const days = dayjs(exclusiveEndDateOf(duration)).diff( + inclusiveStartDateOf(duration), + 'day', + ); + + return [...Array(days).keys()].reduce( + (values: DateAggregation[], i: number): DateAggregation[] => { + const last = values.length ? values[values.length - 1].amount : baseline; + values.push({ + date: dayjs(inclusiveStartDateOf(duration)) + .add(i, 'day') + .format('YYYY-MM-DD'), + amount: last + (baseline / 20) * (Math.random() * 2 - 1), + }); + return values; + }, + [], + ); +} + +function trendlineOf(aggregation: DateAggregation[]): Trendline { + const data: ReadonlyArray = aggregation.map(a => [ + Date.parse(a.date) / 1000, + a.amount, + ]); + const result = regression.linear(data, { precision: 5 }); + return { + slope: result.equation[0], + intercept: result.equation[1], + }; +} + +function changeOf(aggregation: DateAggregation[]): ChangeStatistic { + const half = Math.ceil(aggregation.length / 2); + const before = aggregation + .slice(0, half) + .reduce((sum, a) => sum + a.amount, 0); + const after = aggregation + .slice(half, aggregation.length) + .reduce((sum, a) => sum + a.amount, 0); + return { + ratio: (after - before) / before, + amount: after - before, + }; +} + export class ExampleCostInsightsClient implements CostInsightsApi { private request(_: any, res: any): Promise { return new Promise(resolve => setTimeout(resolve, 0, res)); @@ -53,30 +121,17 @@ export class ExampleCostInsightsClient implements CostInsightsApi { metric: string | null, intervals: string, ): Promise { + const aggregation = aggregationFor( + durationOf(intervals), + metric ? 0.3 : 8_000, + ); const groupDailyCost: Cost = await this.request( { group, metric, intervals }, { id: metric, // costs with null ids will appear as "All Projects" in Cost Overview panel - aggregation: [ - { date: '2020-08-01', amount: 75_000 / (metric ? 200_000 : 1) }, - { date: '2020-08-02', amount: 120_000 / (metric ? 200_000 : 1) }, - { date: '2020-08-03', amount: 110_000 / (metric ? 200_000 : 1) }, - { date: '2020-08-04', amount: 90_000 / (metric ? 200_000 : 1) }, - { date: '2020-08-05', amount: 80_000 / (metric ? 200_000 : 1) }, - { date: '2020-08-06', amount: 85_000 / (metric ? 200_000 : 1) }, - { date: '2020-08-07', amount: 82_500 / (metric ? 200_000 : 1) }, - { date: '2020-08-08', amount: 100_000 / (metric ? 200_000 : 1) }, - { date: '2020-08-09', amount: 130_000 / (metric ? 200_000 : 1) }, - { date: '2020-08-10', amount: 140_000 / (metric ? 200_000 : 1) }, - ], - change: { - ratio: 0.86, - amount: 65_000, - }, - trendline: { - slope: 0, - intercept: 90_000, - }, + aggregation: aggregation, + change: changeOf(aggregation), + trendline: trendlineOf(aggregation), }, ); @@ -88,30 +143,17 @@ export class ExampleCostInsightsClient implements CostInsightsApi { metric: string | null, intervals: string, ): Promise { + const aggregation = aggregationFor( + durationOf(intervals), + metric ? 0.1 : 1_500, + ); const projectDailyCost: Cost = await this.request( { project, metric, intervals }, { id: 'project-a', - aggregation: [ - { date: '2020-08-01', amount: 1000 }, - { date: '2020-08-02', amount: 2000 }, - { date: '2020-08-03', amount: 3000 }, - { date: '2020-08-04', amount: 4000 }, - { date: '2020-08-05', amount: 5000 }, - { date: '2020-08-06', amount: 6000 }, - { date: '2020-08-07', amount: 7000 }, - { date: '2020-08-08', amount: 8000 }, - { date: '2020-08-09', amount: 9000 }, - { date: '2020-08-10', amount: 10_000 }, - ], - change: { - ratio: 0.5, - amount: 10000, - }, - trendline: { - slope: 0, - intercept: 0, - }, + aggregation: aggregation, + change: changeOf(aggregation), + trendline: trendlineOf(aggregation), }, ); @@ -122,9 +164,38 @@ export class ExampleCostInsightsClient implements CostInsightsApi { product: string, group: string, duration: Duration, + project: Maybe, ): Promise { + const projectProductInsights = await this.request( + { product, group, duration, project }, + { + aggregation: [80_000, 110_000], + change: { + ratio: 0.375, + amount: 30_000, + }, + entities: [ + { + id: null, // entities with null ids will be appear as "Unlabeled" in product panels + aggregation: [45_000, 50_000], + }, + { + id: 'entity-a', + aggregation: [15_000, 20_000], + }, + { + id: 'entity-b', + aggregation: [20_000, 30_000], + }, + { + id: 'entity-e', + aggregation: [0, 10_000], + }, + ], + }, + ); const productInsights: ProductCost = await this.request( - { product, group, duration }, + { product, group, duration, project }, { aggregation: [200_000, 250_000], change: { @@ -134,7 +205,7 @@ export class ExampleCostInsightsClient implements CostInsightsApi { entities: [ { id: null, // entities with null ids will be appear as "Unlabeled" in product panels - aggregation: [45_000, 50_000], + aggregation: [15_000, 30_000], }, { id: 'entity-a', @@ -172,36 +243,48 @@ export class ExampleCostInsightsClient implements CostInsightsApi { }, ); - return productInsights; + return project ? projectProductInsights : productInsights; } async getAlerts(group: string): Promise { - const alerts: Alert[] = await this.request({ group }, [ - { - id: 'projectGrowth', - project: 'example-project', - periodStart: 'Q1 2020', - periodEnd: 'Q2 2020', - aggregation: [60_000, 120_000], - change: { - ratio: 1, - amount: 60000, - }, - products: [ - { - id: 'Compute Engine', - aggregation: [58_000, 118_000], - }, - { - id: 'Cloud Dataflow', - aggregation: [1200, 1500], - }, - { - id: 'Cloud Storage', - aggregation: [800, 500], - }, - ], + const projectGrowthData: ProjectGrowthData = { + project: 'example-project', + periodStart: 'Q2 2020', + periodEnd: 'Q3 2020', + aggregation: [60_000, 120_000], + change: { + ratio: 1, + amount: 60000, }, + products: [ + { id: 'Compute Engine', aggregation: [58_000, 118_000] }, + { id: 'Cloud Dataflow', aggregation: [1200, 1500] }, + { id: 'Cloud Storage', aggregation: [800, 500] }, + ], + }; + + const unlabeledDataflowData: UnlabeledDataflowData = { + periodStart: '2020-09-01', + periodEnd: '2020-09-30', + labeledCost: 6_200, + unlabeledCost: 7_000, + projects: [ + { + id: 'example-project-1', + unlabeledCost: 5_000, + labeledCost: 3_000, + }, + { + id: 'example-project-2', + unlabeledCost: 2_000, + labeledCost: 3_200, + }, + ], + }; + + const alerts: Alert[] = await this.request({ group }, [ + new ProjectGrowthAlert(projectGrowthData), + new UnlabeledDataflowAlert(unlabeledDataflowData), ]); return alerts; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 72f047761d..b012b5205b 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.1.1-alpha.23", + "version": "0.1.1-alpha.24", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli-common": "^0.1.1-alpha.23", - "@backstage/config": "^0.1.1-alpha.23", - "@backstage/config-loader": "^0.1.1-alpha.23", + "@backstage/cli-common": "^0.1.1-alpha.24", + "@backstage/config": "^0.1.1-alpha.24", + "@backstage/config-loader": "^0.1.1-alpha.24", "@types/cors": "^2.8.6", "@types/express": "^4.17.6", "compression": "^1.7.4", @@ -39,11 +39,13 @@ "express": "^4.17.1", "express-prom-bundle": "^6.1.0", "express-promise-router": "^3.0.3", + "git-url-parse": "^11.3.0", "helmet": "^4.0.0", "knex": "^0.21.1", "lodash": "^4.17.15", "logform": "^2.1.1", "morgan": "^1.10.0", + "node-fetch": "^2.6.0", "prom-client": "^12.0.0", "selfsigned": "^1.10.7", "stoppable": "^1.1.0", @@ -58,10 +60,11 @@ } }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/cli": "^0.1.1-alpha.24", "@types/compression": "^1.7.0", "@types/http-errors": "^1.6.3", "@types/morgan": "^1.9.0", + "@types/node-fetch": "^2.5.7", "@types/stoppable": "^1.1.0", "@types/supertest": "^2.0.8", "@types/webpack-env": "^1.15.2", @@ -70,6 +73,7 @@ "http-errors": "^1.7.3", "jest": "^26.0.1", "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", "supertest": "^4.0.2" }, "files": [ diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 247eb593cc..7432b204dd 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -21,6 +21,7 @@ import { loadConfig } from '@backstage/config-loader'; * Load configuration for a Backend */ export async function loadBackendConfig() { + /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); const configs = await loadConfig({ env: process.env.NODE_ENV ?? 'development', diff --git a/packages/backend-common/src/database/SingleConnection.test.ts b/packages/backend-common/src/database/SingleConnection.test.ts new file mode 100644 index 0000000000..6278e4ecad --- /dev/null +++ b/packages/backend-common/src/database/SingleConnection.test.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { createDatabaseClient } from './connection'; +import { SingleConnectionDatabaseManager } from './SingleConnection'; + +jest.mock('./connection'); + +describe('SingleConnectionDatabaseManager', () => { + const createConfig = (data: any) => + ConfigReader.fromConfigs([ + { + context: '', + data, + }, + ]); + + const defaultConfigOptions = { + backend: { + database: { + client: 'pg', + connection: { + host: 'localhost', + user: 'foo', + password: 'bar', + database: 'foodb', + }, + }, + }, + }; + const defaultConfig = () => createConfig(defaultConfigOptions); + + // This is similar to the ts-jest `mocked` helper. + const mocked = (f: Function) => f as jest.Mock; + + afterEach(() => jest.resetAllMocks()); + + describe('SingleConnectionDatabaseManager.fromConfig', () => { + it('accesses the backend.database key', () => { + const getConfig = jest.fn(); + const config = defaultConfig(); + config.getConfig = getConfig; + + SingleConnectionDatabaseManager.fromConfig(config); + + expect(getConfig.mock.calls[0][0]).toEqual('backend.database'); + }); + }); + + describe('SingleConnectionDatabaseManager.forPlugin', () => { + const manager = SingleConnectionDatabaseManager.fromConfig(defaultConfig()); + + it('connects to a database scoped to the plugin', async () => { + const pluginId = 'test1'; + await manager.forPlugin(pluginId).getClient(); + + expect(mocked(createDatabaseClient)).toHaveBeenCalledTimes(1); + + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const callArgs = mockCalls[0]; + expect(callArgs[0].get()).toEqual(defaultConfigOptions.backend.database); + expect(callArgs[1].connection.database).toEqual( + `backstage_plugin_${pluginId}`, + ); + }); + + it('provides different plugins different databases', async () => { + const plugin1Id = 'test1'; + const plugin2Id = 'test2'; + await manager.forPlugin(plugin1Id).getClient(); + await manager.forPlugin(plugin2Id).getClient(); + + expect(mocked(createDatabaseClient)).toHaveBeenCalledTimes(2); + + const mockCalls = mocked(createDatabaseClient).mock.calls; + const plugin1CallArgs = mockCalls[0]; + const plugin2CallArgs = mockCalls[1]; + expect(plugin1CallArgs[1].connection.database).not.toEqual( + plugin2CallArgs[1].connection.database, + ); + }); + }); +}); diff --git a/packages/backend-common/src/database/SingleConnection.ts b/packages/backend-common/src/database/SingleConnection.ts new file mode 100644 index 0000000000..777ced54a0 --- /dev/null +++ b/packages/backend-common/src/database/SingleConnection.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Knex from 'knex'; +import { Config } from '@backstage/config'; +import { createDatabaseClient, ensureDatabaseExists } from './connection'; +import { PluginDatabaseManager } from './types'; + +/** + * Implements a Database Manager which will automatically create new databases + * for plugins when requested. All requested databases are created with the + * credentials provided; if the database already exists no attempt to create + * the database will be made. + */ +export class SingleConnectionDatabaseManager { + /** + * Creates a new SingleConnectionDatabaseManager instance by reading from the `backend` + * config section, specifically the `.database` key for discovering the management + * database configuration. + * + * @param config The loaded application configuration. + */ + static fromConfig(config: Config): SingleConnectionDatabaseManager { + return new SingleConnectionDatabaseManager( + config.getConfig('backend.database'), + ); + } + + private constructor(private readonly config: Config) {} + + /** + * Generates a PluginDatabaseManager for consumption by plugins. + * + * @param pluginId The plugin that the database manager should be created for. Plugin names should be unique. + */ + forPlugin(pluginId: string): PluginDatabaseManager { + const _this = this; + + return { + getClient(): Promise { + return _this.getDatabase(pluginId); + }, + }; + } + + private async getDatabase(pluginId: string): Promise { + const config = this.config; + const overrides = SingleConnectionDatabaseManager.getDatabaseOverrides( + pluginId, + ); + const overrideConfig = overrides.connection as Knex.ConnectionConfig; + await this.ensureDatabase(overrideConfig.database); + + return createDatabaseClient(config, overrides); + } + + private static getDatabaseOverrides(pluginId: string): Knex.Config { + return { + connection: { + database: `backstage_plugin_${pluginId}`, + }, + }; + } + + private async ensureDatabase(database: string) { + const config = this.config; + await ensureDatabaseExists(config, database); + } +} diff --git a/packages/backend-common/src/database/config.test.ts b/packages/backend-common/src/database/config.test.ts index ab29bc19ba..c9a4107686 100644 --- a/packages/backend-common/src/database/config.test.ts +++ b/packages/backend-common/src/database/config.test.ts @@ -18,6 +18,18 @@ import { mergeDatabaseConfig } from './config'; describe('config', () => { describe(mergeDatabaseConfig, () => { + it('does not mutate the input object', () => { + const input = { + original: 'key', + }; + const override = { + added: 'value', + }; + + mergeDatabaseConfig(input, override); + expect(input).not.toHaveProperty('added'); + }); + it('does not require overrides', () => { expect( mergeDatabaseConfig({ diff --git a/packages/backend-common/src/database/config.ts b/packages/backend-common/src/database/config.ts index 80abc06aa2..af32556c28 100644 --- a/packages/backend-common/src/database/config.ts +++ b/packages/backend-common/src/database/config.ts @@ -19,9 +19,9 @@ import { merge } from 'lodash'; /** * Merges database objects together * - * @param config The base config + * @param config The base config. The input is not modified * @param overrides Any additional overrides */ export function mergeDatabaseConfig(config: any, ...overrides: any[]) { - return merge(config, ...overrides); + return merge({}, config, ...overrides); } diff --git a/packages/backend-common/src/database/index.ts b/packages/backend-common/src/database/index.ts index 38d3d6224b..c81153aa62 100644 --- a/packages/backend-common/src/database/index.ts +++ b/packages/backend-common/src/database/index.ts @@ -15,3 +15,5 @@ */ export * from './connection'; +export * from './types'; +export * from './SingleConnection'; diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts new file mode 100644 index 0000000000..7eaa05f173 --- /dev/null +++ b/packages/backend-common/src/database/types.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import knex from 'knex'; + +/** + * The PluginDatabaseManager manages access to databases that Plugins get. + */ +export interface PluginDatabaseManager { + /** + * getClient provides backend plugins database connections for itself. + * + * The purpose of this method is to allow plugins to get isolated data + * stores so that plugins are discouraged from database integration. + */ + getClient(): Promise; +} diff --git a/packages/backend-common/src/index.ts b/packages/backend-common/src/index.ts index 7074040655..e968edef69 100644 --- a/packages/backend-common/src/index.ts +++ b/packages/backend-common/src/index.ts @@ -20,6 +20,7 @@ export * from './discovery'; export * from './errors'; export * from './logging'; export * from './middleware'; +export * from './reading'; export * from './service'; export * from './paths'; export * from './hot'; diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts new file mode 100644 index 0000000000..c9e1fc5bc7 --- /dev/null +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '../logging'; +import { AzureUrlReader } from './AzureUrlReader'; + +const logger = getVoidLogger(); + +describe('AzureUrlReader', () => { + const worker = setupServer(); + + beforeAll(() => worker.listen({ onUnhandledRequest: 'error' })); + afterAll(() => worker.close()); + + beforeEach(() => { + worker.use( + rest.get('*', (req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + url: req.url.toString(), + headers: req.headers.getAllHeaders(), + }), + ), + ), + ); + }); + afterEach(() => worker.resetHandlers()); + + const createConfig = (token?: string) => + new ConfigReader( + { + integrations: { azure: [{ host: 'dev.azure.com', token }] }, + }, + 'test-config', + ); + + it.each([ + { + url: + 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster', + config: createConfig(), + response: expect.objectContaining({ + url: + 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master', + }), + }, + { + url: + 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml', + config: createConfig(), + response: expect.objectContaining({ + url: + 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml', + }), + }, + { + url: 'https://dev.azure.com/a/b/_git/repo-name?path=my-template.yaml', + config: createConfig('0123456789'), + response: expect.objectContaining({ + headers: expect.objectContaining({ + authorization: 'Basic OjAxMjM0NTY3ODk=', + }), + }), + }, + { + url: 'https://dev.azure.com/a/b/_git/repo-name?path=my-template.yaml', + config: createConfig(undefined), + response: expect.objectContaining({ + headers: expect.not.objectContaining({ + authorization: expect.anything(), + }), + }), + }, + ])('should handle happy path %#', async ({ url, config, response }) => { + const [{ reader }] = AzureUrlReader.factory({ config, logger }); + + const data = await reader.read(url); + const res = await JSON.parse(data.toString('utf-8')); + expect(res).toEqual(response); + }); + + it.each([ + { + url: 'https://api.com/a/b/blob/master/path/to/c.yaml', + config: createConfig(), + error: + 'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path', + }, + { + url: 'com/a/b/blob/master/path/to/c.yaml', + config: createConfig(), + error: + 'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml', + }, + { + url: '', + config: createConfig(''), + error: + "Invalid type in config for key 'integrations.azure[0].token' in 'test-config', got empty-string, wanted string", + }, + ])('should handle error path %#', async ({ url, config, error }) => { + await expect(async () => { + const [{ reader }] = AzureUrlReader.factory({ config, logger }); + await reader.read(url); + }).rejects.toThrow(error); + }); +}); diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts new file mode 100644 index 0000000000..28fbf25eea --- /dev/null +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -0,0 +1,165 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fetch, { RequestInit, HeadersInit, Response } from 'node-fetch'; +import { Config } from '@backstage/config'; +import { NotFoundError } from '../errors'; +import { ReaderFactory, UrlReader } from './types'; + +type Options = { + // TODO: added here for future support, but we only allow dev.azure.com for now + host: string; + token?: string; +}; + +function readConfig(config: Config): Options[] { + const optionsArr = Array(); + + const providerConfigs = + config.getOptionalConfigArray('integrations.azure') ?? []; + + for (const providerConfig of providerConfigs) { + const host = providerConfig.getOptionalString('host') ?? 'dev.azure.com'; + const token = providerConfig.getOptionalString('token'); + + optionsArr.push({ host, token }); + } + + // As a convenience we always make sure there's at least an unauthenticated + // reader for public azure repos. + if (!optionsArr.some(p => p.host === 'dev.azure.com')) { + optionsArr.push({ host: 'dev.azure.com' }); + } + + return optionsArr; +} + +export class AzureUrlReader implements UrlReader { + static factory: ReaderFactory = ({ config }) => { + return readConfig(config).map(options => { + const reader = new AzureUrlReader(options); + const predicate = (url: URL) => url.host === options.host; + return { reader, predicate }; + }); + }; + + constructor(private readonly options: Options) { + if (options.host !== 'dev.azure.com') { + throw Error( + `Azure integration currently only supports 'dev.azure.com', tried to use host '${options.host}'`, + ); + } + } + + async read(url: string): Promise { + const builtUrl = this.buildRawUrl(url); + + let response: Response; + try { + response = await fetch(builtUrl.toString(), this.getRequestOptions()); + } catch (e) { + throw new Error(`Unable to read ${url}, ${e}`); + } + + // 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 response.buffer(); + } + + const message = `${url} could not be read as ${builtUrl}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + // Converts + // from: https://dev.azure.com/{organization}/{project}/_git/reponame?path={path}&version=GB{commitOrBranch}&_a=contents + // to: https://dev.azure.com/{organization}/{project}/_apis/git/repositories/reponame/items?path={path}&version={commitOrBranch} + private buildRawUrl(target: string): URL { + try { + const url = new URL(target); + + const [ + empty, + userOrOrg, + project, + srcKeyword, + repoName, + ] = url.pathname.split('/'); + + const path = url.searchParams.get('path') || ''; + const ref = url.searchParams.get('version')?.substr(2); + + if ( + url.hostname !== 'dev.azure.com' || + empty !== '' || + userOrOrg === '' || + project === '' || + srcKeyword !== '_git' || + repoName === '' || + path === '' || + ref === '' + ) { + throw new Error('Wrong Azure Devops URL or Invalid file path'); + } + + // transform to api + url.pathname = [ + empty, + userOrOrg, + project, + '_apis', + 'git', + 'repositories', + repoName, + 'items', + ].join('/'); + + const queryParams = [`path=${path}`]; + + if (ref) { + queryParams.push(`version=${ref}`); + } + + url.search = queryParams.join('&'); + + url.protocol = 'https'; + + return url; + } catch (e) { + throw new Error(`Incorrect url: ${target}, ${e}`); + } + } + + private getRequestOptions(): RequestInit { + const headers: HeadersInit = {}; + + if (this.options.token) { + headers.Authorization = `Basic ${Buffer.from( + `:${this.options.token}`, + 'utf8', + ).toString('base64')}`; + } + + return { headers }; + } + + toString() { + const { host, token } = this.options; + return `azure{host=${host},authed=${Boolean(token)}}`; + } +} diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts new file mode 100644 index 0000000000..c3e61fb821 --- /dev/null +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -0,0 +1,153 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '../logging'; +import { BitbucketUrlReader } from './BitbucketUrlReader'; + +const logger = getVoidLogger(); + +describe('BitbucketUrlReader', () => { + const worker = setupServer(); + + beforeAll(() => worker.listen({ onUnhandledRequest: 'error' })); + afterAll(() => worker.close()); + + beforeEach(() => { + worker.use( + rest.get('*', (req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + url: req.url.toString(), + headers: req.headers.getAllHeaders(), + }), + ), + ), + ); + }); + afterEach(() => worker.resetHandlers()); + + const createConfig = (username?: string, appPassword?: string) => + new ConfigReader( + { + integrations: { + bitbucket: [ + { + host: 'bitbucket.org', + username: username, + appPassword: appPassword, + }, + ], + }, + }, + 'test-config', + ); + + it.each([ + { + url: + 'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml', + config: createConfig(), + response: expect.objectContaining({ + url: + 'https://api.bitbucket.org/2.0/repositories/org-name/repo-name/src/master/templates/my-template.yaml', + }), + }, + { + url: + 'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml', + config: createConfig('some-user', 'my-secret'), + response: expect.objectContaining({ + headers: expect.objectContaining({ + authorization: 'Basic c29tZS11c2VyOm15LXNlY3JldA==', + }), + }), + }, + { + url: + 'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml', + config: createConfig(), + response: expect.objectContaining({ + headers: expect.not.objectContaining({ + authorization: expect.anything(), + }), + }), + }, + { + url: + 'https://bitbucket.org/org-name/repo-name/src/master/templates/my-template.yaml', + config: createConfig(undefined, 'only-password-provided'), + response: expect.objectContaining({ + headers: expect.not.objectContaining({ + authorization: expect.anything(), + }), + }), + }, + ])('should handle happy path %#', async ({ url, config, response }) => { + const [{ reader }] = BitbucketUrlReader.factory({ config, logger }); + + const data = await reader.read(url); + const res = await JSON.parse(data.toString('utf-8')); + expect(res).toEqual(response); + }); + + it.each([ + { + url: 'https://api.com/a/b/blob/master/path/to/c.yaml', + config: createConfig(), + error: + 'Incorrect url: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Bitbucket URL or Invalid file path', + }, + { + url: 'com/a/b/blob/master/path/to/c.yaml', + config: createConfig(), + error: + 'Incorrect url: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml', + }, + { + url: '', + config: createConfig('', ''), + error: + "Invalid type in config for key 'integrations.bitbucket[0].username' in 'test-config', got empty-string, wanted string", + }, + { + url: '', + config: createConfig('only-user-provided', ''), + error: + "Invalid type in config for key 'integrations.bitbucket[0].appPassword' in 'test-config', got empty-string, wanted string", + }, + { + url: '', + config: createConfig('', 'only-password-provided'), + error: + "Invalid type in config for key 'integrations.bitbucket[0].username' in 'test-config', got empty-string, wanted string", + }, + { + url: '', + config: createConfig('only-user-provided', undefined), + error: + "Missing required config value at 'integrations.bitbucket[0].appPassword'", + }, + ])('should handle error path %#', async ({ url, config, error }) => { + await expect(async () => { + const [{ reader }] = BitbucketUrlReader.factory({ config, logger }); + await reader.read(url); + }).rejects.toThrow(error); + }); +}); diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts new file mode 100644 index 0000000000..e2576eed47 --- /dev/null +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -0,0 +1,162 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fetch, { RequestInit, HeadersInit, Response } from 'node-fetch'; +import { Config } from '@backstage/config'; +import { ReaderFactory, UrlReader } from './types'; +import { NotFoundError } from '../errors'; + +type Options = { + // TODO: added here for future support, but we only allow bitbucket.org for now + host: string; + auth?: { + username: string; + appPassword: string; + }; +}; + +function readConfig(config: Config): Options[] { + const optionsArr = Array(); + + const providerConfigs = + config.getOptionalConfigArray('integrations.bitbucket') ?? []; + + for (const providerConfig of providerConfigs) { + const host = providerConfig.getOptionalString('host') ?? 'bitbucket.org'; + + let auth; + if (providerConfig.has('username')) { + const username = providerConfig.getString('username'); + const appPassword = providerConfig.getString('appPassword'); + auth = { username, appPassword }; + } + + optionsArr.push({ host, auth }); + } + + // As a convenience we always make sure there's at least an unauthenticated + // reader for public bitbucket repos. + if (!optionsArr.some(p => p.host === 'bitbucket.org')) { + optionsArr.push({ host: 'bitbucket.org' }); + } + + return optionsArr; +} + +export class BitbucketUrlReader implements UrlReader { + static factory: ReaderFactory = ({ config }) => { + return readConfig(config).map(options => { + const reader = new BitbucketUrlReader(options); + const predicate = (url: URL) => url.host === options.host; + return { reader, predicate }; + }); + }; + + constructor(private readonly options: Options) { + if (options.host !== 'bitbucket.org') { + throw Error( + `Bitbucket integration currently only supports 'bitbucket.org', tried to use host '${options.host}'`, + ); + } + } + + async read(url: string): Promise { + const builtUrl = this.buildRawUrl(url); + + let response: Response; + try { + response = await fetch(builtUrl.toString(), this.getRequestOptions()); + } catch (e) { + throw new Error(`Unable to read ${url}, ${e}`); + } + + if (response.ok) { + return response.buffer(); + } + + const message = `${url} could not be read as ${builtUrl}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + // Converts + // from: https://bitbucket.org/orgname/reponame/src/master/file.yaml + // to: https://api.bitbucket.org/2.0/repositories/orgname/reponame/src/master/file.yaml + private buildRawUrl(target: string): URL { + try { + const url = new URL(target); + + const [ + empty, + userOrOrg, + repoName, + srcKeyword, + ref, + ...restOfPath + ] = url.pathname.split('/'); + + if ( + url.hostname !== 'bitbucket.org' || + empty !== '' || + userOrOrg === '' || + repoName === '' || + srcKeyword !== 'src' + ) { + throw new Error('Wrong Bitbucket URL or Invalid file path'); + } + + // transform to api + url.pathname = [ + empty, + '2.0', + 'repositories', + userOrOrg, + repoName, + 'src', + ref, + ...restOfPath, + ].join('/'); + url.hostname = 'api.bitbucket.org'; + url.protocol = 'https'; + + return url; + } catch (e) { + throw new Error(`Incorrect url: ${target}, ${e}`); + } + } + + private getRequestOptions(): RequestInit { + const headers: HeadersInit = {}; + + if (this.options.auth) { + headers.Authorization = `Basic ${Buffer.from( + `${this.options.auth.username}:${this.options.auth.appPassword}`, + 'utf8', + ).toString('base64')}`; + } + + return { + headers, + }; + } + + toString() { + const { host, auth } = this.options; + return `bitbucket{host=${host},authed=${Boolean(auth)}}`; + } +} diff --git a/packages/backend-common/src/reading/FetchUrlReader.ts b/packages/backend-common/src/reading/FetchUrlReader.ts new file mode 100644 index 0000000000..ea56f2182d --- /dev/null +++ b/packages/backend-common/src/reading/FetchUrlReader.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fetch, { Response } from 'node-fetch'; +import { NotFoundError } from '../errors'; +import { UrlReader } from './types'; + +/** + * A UrlReader that does a plain fetch of the URL. + */ +export class FetchUrlReader implements UrlReader { + async read(url: string): Promise { + let response: Response; + try { + response = await fetch(url); + } catch (e) { + throw new Error(`Unable to read ${url}, ${e}`); + } + + if (response.ok) { + return response.buffer(); + } + + const message = `could not read ${url}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + toString() { + return 'fetch{}'; + } +} diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts similarity index 66% rename from plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.test.ts rename to packages/backend-common/src/reading/GithubUrlReader.test.ts index 8eb46db967..8df464db9f 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubReaderProcessor.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -14,23 +14,21 @@ * limitations under the License. */ -import { getVoidLogger } from '@backstage/backend-common'; -import { LocationSpec } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import { getApiRequestOptions, getApiUrl, getRawRequestOptions, getRawUrl, - GithubReaderProcessor, + GithubUrlReader, ProviderConfig, readConfig, -} from './GithubReaderProcessor'; +} from './GithubUrlReader'; -describe('GithubReaderProcessor', () => { +describe('GithubUrlReader', () => { describe('getApiRequestOptions', () => { it('sets the correct API version', () => { - const config: ProviderConfig = { target: '', apiBaseUrl: '' }; + const config: ProviderConfig = { host: '', apiBaseUrl: '' }; expect((getApiRequestOptions(config).headers as any).Accept).toEqual( 'application/vnd.github.v3.raw', ); @@ -38,12 +36,12 @@ describe('GithubReaderProcessor', () => { it('inserts a token when needed', () => { const withToken: ProviderConfig = { - target: '', + host: '', apiBaseUrl: '', token: 'A', }; const withoutToken: ProviderConfig = { - target: '', + host: '', apiBaseUrl: '', }; expect( @@ -58,12 +56,12 @@ describe('GithubReaderProcessor', () => { describe('getRawRequestOptions', () => { it('inserts a token when needed', () => { const withToken: ProviderConfig = { - target: '', + host: '', rawBaseUrl: '', token: 'A', }; const withoutToken: ProviderConfig = { - target: '', + host: '', rawBaseUrl: '', }; expect( @@ -77,13 +75,13 @@ describe('GithubReaderProcessor', () => { describe('getApiUrl', () => { it('rejects targets that do not look like URLs', () => { - const config: ProviderConfig = { target: '', apiBaseUrl: '' }; + const config: ProviderConfig = { host: '', apiBaseUrl: '' }; expect(() => getApiUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/); }); it('happy path for github', () => { const config: ProviderConfig = { - target: 'https://github.com', + host: 'github.com', apiBaseUrl: 'https://api.github.com', }; expect( @@ -110,7 +108,7 @@ describe('GithubReaderProcessor', () => { it('happy path for ghe', () => { const config: ProviderConfig = { - target: 'https://ghe.mycompany.net', + host: 'ghe.mycompany.net', apiBaseUrl: 'https://ghe.mycompany.net/api/v3', }; expect( @@ -128,13 +126,13 @@ describe('GithubReaderProcessor', () => { describe('getRawUrl', () => { it('rejects targets that do not look like URLs', () => { - const config: ProviderConfig = { target: '', apiBaseUrl: '' }; + const config: ProviderConfig = { host: '', apiBaseUrl: '' }; expect(() => getRawUrl('a/b', config)).toThrow(/Incorrect URL: a\/b/); }); it('happy path for github', () => { const config: ProviderConfig = { - target: 'https://github.com', + host: 'github.com', rawBaseUrl: 'https://raw.githubusercontent.com', }; expect( @@ -151,7 +149,7 @@ describe('GithubReaderProcessor', () => { it('happy path for ghe', () => { const config: ProviderConfig = { - target: 'https://ghe.mycompany.net', + host: 'ghe.mycompany.net', rawBaseUrl: 'https://ghe.mycompany.net/raw', }; expect( @@ -167,23 +165,23 @@ describe('GithubReaderProcessor', () => { describe('readConfig', () => { function config( - providers: { target: string; apiBaseUrl?: string; token?: string }[], + providers: { host: string; apiBaseUrl?: string; token?: string }[], ) { return ConfigReader.fromConfigs([ { context: '', data: { - catalog: { processors: { github: { providers } } }, + integrations: { github: providers }, }, }, ]); } it('adds a default GitHub entry when missing', () => { - const output = readConfig(config([]), getVoidLogger()); + const output = readConfig(config([])); expect(output).toEqual([ { - target: 'https://github.com', + host: 'github.com', apiBaseUrl: 'https://api.github.com', rawBaseUrl: 'https://raw.githubusercontent.com', }, @@ -191,13 +189,10 @@ describe('GithubReaderProcessor', () => { }); it('injects the correct GitHub API base URL when missing', () => { - const output = readConfig( - config([{ target: 'https://github.com' }]), - getVoidLogger(), - ); + const output = readConfig(config([{ host: 'github.com' }])); expect(output).toEqual([ { - target: 'https://github.com', + host: 'github.com', apiBaseUrl: 'https://api.github.com', rawBaseUrl: 'https://raw.githubusercontent.com', }, @@ -205,64 +200,33 @@ describe('GithubReaderProcessor', () => { }); it('rejects custom targets with no base URLs', () => { - expect(() => - readConfig( - config([{ target: 'https://ghe.company.com' }]), - getVoidLogger(), - ), - ).toThrow( - 'Provider at https://ghe.company.com must configure an explicit apiBaseUrl or rawBaseUrl', + expect(() => readConfig(config([{ host: 'ghe.company.com' }]))).toThrow( + "GitHub integration for 'ghe.company.com' must configure an explicit apiBaseUrl and rawBaseUrl", ); }); it('rejects funky configs', () => { + expect(() => readConfig(config([{ host: 7 } as any]))).toThrow(/host/); + expect(() => readConfig(config([{ token: 7 } as any]))).toThrow(/token/); expect(() => - readConfig(config([{ target: 7 } as any]), getVoidLogger()), - ).toThrow(/target/); - expect(() => - readConfig(config([{ noTarget: '7' } as any]), getVoidLogger()), - ).toThrow(/target/); - expect(() => - readConfig( - config([{ target: 'https://github.com', apiBaseUrl: 7 } as any]), - getVoidLogger(), - ), + readConfig(config([{ host: 'github.com', apiBaseUrl: 7 } as any])), ).toThrow(/apiBaseUrl/); expect(() => - readConfig( - config([{ target: 'https://github.com', token: 7 } as any]), - getVoidLogger(), - ), + readConfig(config([{ host: 'github.com', token: 7 } as any])), ).toThrow(/token/); }); }); describe('implementation', () => { - it('rejects unknown types', async () => { - const processor = new GithubReaderProcessor([ - { target: 'https://github.com', apiBaseUrl: 'https://api.github.com' }, - ]); - const location: LocationSpec = { - type: 'not-github', - target: 'https://github.com', - }; - await expect( - processor.readLocation(location, false, () => {}), - ).resolves.toBeFalsy(); - }); - it('rejects unknown targets', async () => { - const processor = new GithubReaderProcessor([ - { target: 'https://github.com', apiBaseUrl: 'https://api.github.com' }, - ]); - const location: LocationSpec = { - type: 'github', - target: 'https://not.github.com/apa', - }; + const processor = new GithubUrlReader({ + host: 'github.com', + apiBaseUrl: 'https://api.github.com', + }); await expect( - processor.readLocation(location, false, () => {}), + processor.read('https://not.github.com/apa'), ).rejects.toThrow( - /There is no GitHub provider that matches https:\/\/not.github.com\/apa/, + 'Incorrect URL: https://not.github.com/apa, Error: Invalid GitHub URL or file path', ); }); }); diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts new file mode 100644 index 0000000000..e5bed6dd26 --- /dev/null +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -0,0 +1,236 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import parseGitUri from 'git-url-parse'; +import fetch, { HeadersInit, RequestInit, Response } from 'node-fetch'; +import { NotFoundError } from '../errors'; +import { ReaderFactory, UrlReader } from './types'; + +/** + * The configuration parameters for a single GitHub API provider. + */ +export type ProviderConfig = { + /** + * The host of the target that this matches on, e.g. "github.com" + */ + host: string; + + /** + * The base URL of the API of this provider, e.g. "https://api.github.com", + * with no trailing slash. + * + * May be omitted specifically for GitHub; then it will be deduced. + * + * The API will always be preferred if both its base URL and a token are + * present. + */ + apiBaseUrl?: string; + + /** + * The base URL of the raw fetch endpoint of this provider, e.g. + * "https://raw.githubusercontent.com", with no trailing slash. + * + * May be omitted specifically for GitHub; then it will be deduced. + * + * The API will always be preferred if both its base URL and a token are + * present. + */ + rawBaseUrl?: string; + + /** + * The authorization token to use for requests to this provider. + * + * If no token is specified, anonymous access is used. + */ + token?: string; +}; + +export function getApiRequestOptions(provider: ProviderConfig): RequestInit { + const headers: HeadersInit = { + Accept: 'application/vnd.github.v3.raw', + }; + + if (provider.token) { + headers.Authorization = `token ${provider.token}`; + } + + return { + headers, + }; +} + +export function getRawRequestOptions(provider: ProviderConfig): RequestInit { + const headers: HeadersInit = {}; + + if (provider.token) { + headers.Authorization = `token ${provider.token}`; + } + + return { + headers, + }; +} + +// Converts for example +// from: https://github.com/a/b/blob/branchname/path/to/c.yaml +// to: https://api.github.com/repos/a/b/contents/path/to/c.yaml?ref=branchname +export function getApiUrl(target: string, provider: ProviderConfig): URL { + try { + const { owner, name, ref, filepathtype, filepath } = parseGitUri(target); + + if ( + !owner || + !name || + !ref || + (filepathtype !== 'blob' && filepathtype !== 'raw') + ) { + throw new Error('Invalid GitHub URL or file path'); + } + + const pathWithoutSlash = filepath.replace(/^\//, ''); + return new URL( + `${provider.apiBaseUrl}/repos/${owner}/${name}/contents/${pathWithoutSlash}?ref=${ref}`, + ); + } catch (e) { + throw new Error(`Incorrect URL: ${target}, ${e}`); + } +} + +// Converts for example +// from: https://github.com/a/b/blob/branchname/c.yaml +// to: https://raw.githubusercontent.com/a/b/branchname/c.yaml +export function getRawUrl(target: string, provider: ProviderConfig): URL { + try { + const { owner, name, ref, filepathtype, filepath } = parseGitUri(target); + + if ( + !owner || + !name || + !ref || + (filepathtype !== 'blob' && filepathtype !== 'raw') + ) { + throw new Error('Invalid GitHub URL or file path'); + } + + const pathWithoutSlash = filepath.replace(/^\//, ''); + return new URL( + `${provider.rawBaseUrl}/${owner}/${name}/${ref}/${pathWithoutSlash}`, + ); + } catch (e) { + throw new Error(`Incorrect URL: ${target}, ${e}`); + } +} + +export function readConfig(config: Config): ProviderConfig[] { + const providers: ProviderConfig[] = []; + + const providerConfigs = + config.getOptionalConfigArray('integrations.github') ?? []; + + // First read all the explicit providers + for (const providerConfig of providerConfigs) { + const host = providerConfig.getOptionalString('host') ?? 'github.com'; + let apiBaseUrl = providerConfig.getOptionalString('apiBaseUrl'); + let rawBaseUrl = providerConfig.getOptionalString('rawBaseUrl'); + const token = providerConfig.getOptionalString('token'); + + if (apiBaseUrl) { + apiBaseUrl = apiBaseUrl.replace(/\/+$/, ''); + } else if (host === 'github.com') { + apiBaseUrl = 'https://api.github.com'; + } + + if (rawBaseUrl) { + rawBaseUrl = rawBaseUrl.replace(/\/+$/, ''); + } else if (host === 'github.com') { + rawBaseUrl = 'https://raw.githubusercontent.com'; + } + + if (!apiBaseUrl && !rawBaseUrl) { + throw new Error( + `GitHub integration for '${host}' must configure an explicit apiBaseUrl and rawBaseUrl`, + ); + } + + providers.push({ host, apiBaseUrl, rawBaseUrl, token }); + } + + // If no explicit github.com provider was added, put one in the list as + // a convenience + if (!providers.some(p => p.host === 'github.com')) { + providers.push({ + host: 'github.com', + apiBaseUrl: 'https://api.github.com', + rawBaseUrl: 'https://raw.githubusercontent.com', + }); + } + + return providers; +} + +/** + * A processor that adds the ability to read files from GitHub v3 APIs, such as + * the one exposed by GitHub itself. + */ +export class GithubUrlReader implements UrlReader { + private config: ProviderConfig; + + static factory: ReaderFactory = ({ config }) => { + return readConfig(config).map(provider => { + const reader = new GithubUrlReader(provider); + const predicate = (url: URL) => url.host === provider.host; + return { reader, predicate }; + }); + }; + + constructor(config: ProviderConfig) { + this.config = config; + } + + async read(url: string): Promise { + const useApi = + this.config.apiBaseUrl && (this.config.token || !this.config.rawBaseUrl); + const ghUrl = useApi + ? getApiUrl(url, this.config) + : getRawUrl(url, this.config); + const options = useApi + ? getApiRequestOptions(this.config) + : getRawRequestOptions(this.config); + + let response: Response; + try { + response = await fetch(ghUrl.toString(), options); + } catch (e) { + throw new Error(`Unable to read ${url}, ${e}`); + } + + if (response.ok) { + return response.buffer(); + } + + const message = `${url} could not be read as ${ghUrl}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + toString() { + const { host, token } = this.config; + return `github{host=${host},authed=${Boolean(token)}}`; + } +} diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts new file mode 100644 index 0000000000..09da9c4e0a --- /dev/null +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -0,0 +1,122 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '../logging'; +import { GitlabUrlReader } from './GitlabUrlReader'; + +const logger = getVoidLogger(); + +describe('GitlabUrlReader', () => { + const worker = setupServer(); + + beforeAll(() => worker.listen({ onUnhandledRequest: 'error' })); + afterAll(() => worker.close()); + + beforeEach(() => { + worker.use( + rest.get('*/api/v4/projects/:name', (_, res, ctx) => + res(ctx.status(200), ctx.json({ id: 12345 })), + ), + rest.get('*', (req, res, ctx) => + res( + ctx.status(200), + ctx.json({ + url: req.url.toString(), + headers: req.headers.getAllHeaders(), + }), + ), + ), + ); + }); + afterEach(() => worker.resetHandlers()); + + const createConfig = (token?: string) => + new ConfigReader( + { + integrations: { gitlab: [{ host: 'gitlab.com', token }] }, + }, + 'test-config', + ); + + it.each([ + // Project URLs + { + url: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + config: createConfig(), + response: expect.objectContaining({ + url: + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + headers: expect.objectContaining({ + 'private-token': '', + }), + }), + }, + { + url: + 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + config: createConfig('0123456789'), + response: expect.objectContaining({ + url: + 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + headers: expect.objectContaining({ + 'private-token': '0123456789', + }), + }), + }, + { + url: + 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup + config: createConfig(), + response: expect.objectContaining({ + url: + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + }), + }, + + // Raw URLs + { + url: 'https://gitlab.example.com/a/b/blob/master/c.yaml', + config: createConfig(), + response: expect.objectContaining({ + url: 'https://gitlab.example.com/a/b/raw/master/c.yaml', + }), + }, + ])('should handle happy path %#', async ({ url, config, response }) => { + const [{ reader }] = GitlabUrlReader.factory({ config, logger }); + + const data = await reader.read(url); + const res = await JSON.parse(data.toString('utf-8')); + expect(res).toEqual(response); + }); + + it.each([ + { + url: '', + config: createConfig(''), + error: + "Invalid type in config for key 'integrations.gitlab[0].token' in 'test-config', got empty-string, wanted string", + }, + ])('should handle error path %#', async ({ url, config, error }) => { + await expect(async () => { + const [{ reader }] = GitlabUrlReader.factory({ config, logger }); + await reader.read(url); + }).rejects.toThrow(error); + }); +}); diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts new file mode 100644 index 0000000000..0e430d650c --- /dev/null +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -0,0 +1,197 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fetch, { RequestInit, Response } from 'node-fetch'; +import { Config } from '@backstage/config'; +import { NotFoundError } from '../errors'; +import { ReaderFactory, UrlReader } from './types'; + +type Options = { + host: string; + token?: string; +}; + +function readConfig(config: Config): Options[] { + const optionsArr = Array(); + + const providerConfigs = + config.getOptionalConfigArray('integrations.gitlab') ?? []; + + for (const providerConfig of providerConfigs) { + const host = providerConfig.getOptionalString('host') ?? 'gitlab.com'; + const token = providerConfig.getOptionalString('token'); + + optionsArr.push({ host, token }); + } + + // As a convenience we always make sure there's at least an unauthenticated + // reader for public gitlab repos. + if (!optionsArr.some(p => p.host === 'gitlab.com')) { + optionsArr.push({ host: 'gitlab.com' }); + } + + return optionsArr; +} + +export class GitlabUrlReader implements UrlReader { + static factory: ReaderFactory = ({ config }) => { + return readConfig(config).map(options => { + const reader = new GitlabUrlReader(options); + const predicate = (url: URL) => url.host === options.host; + return { reader, predicate }; + }); + }; + + constructor(private readonly options: Options) {} + + async read(url: string): Promise { + // TODO(Rugvip): merged the old GitlabReaderProcessor in here and used + // the existence of /~/blob/ to switch the logic. Don't know if this + // makes sense and it might require some more work. + let builtUrl: URL; + if (url.includes('/-/blob/')) { + const projectID = await this.getProjectID(url); + builtUrl = this.buildProjectUrl(url, projectID); + } else { + builtUrl = this.buildRawUrl(url); + } + + let response: Response; + try { + response = await fetch(builtUrl.toString(), this.getRequestOptions()); + } catch (e) { + throw new Error(`Unable to read ${url}, ${e}`); + } + + if (response.ok) { + return response.buffer(); + } + + const message = `${url} could not be read as ${builtUrl}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + // Converts + // from: https://gitlab.example.com/a/b/blob/master/c.yaml + // to: https://gitlab.example.com/a/b/raw/master/c.yaml + private buildRawUrl(target: string): URL { + try { + const url = new URL(target); + + const [ + empty, + userOrOrg, + repoName, + blobKeyword, + ...restOfPath + ] = url.pathname.split('/'); + + if ( + empty !== '' || + userOrOrg === '' || + repoName === '' || + blobKeyword !== 'blob' || + !restOfPath.join('/').match(/\.yaml$/) + ) { + throw new Error('Wrong GitLab URL'); + } + + // Replace 'blob' with 'raw' + url.pathname = [empty, userOrOrg, repoName, 'raw', ...restOfPath].join( + '/', + ); + + return url; + } catch (e) { + throw new Error(`Incorrect url: ${target}, ${e}`); + } + } + + // convert https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath + // to https://gitlab.com/api/v4/projects//repository/files/filepath?ref=branch + private buildProjectUrl(target: string, projectID: Number): URL { + try { + const url = new URL(target); + + const branchAndfilePath = url.pathname.split('/-/blob/')[1]; + + const [branch, ...filePath] = branchAndfilePath.split('/'); + + url.pathname = [ + '/api/v4/projects', + projectID, + 'repository/files', + encodeURIComponent(filePath.join('/')), + 'raw', + ].join('/'); + url.search = `?ref=${branch}`; + + return url; + } catch (e) { + throw new Error(`Incorrect url: ${target}, ${e}`); + } + } + + private async getProjectID(target: string): Promise { + const url = new URL(target); + + if ( + // absPaths to gitlab files should contain /-/blob + // ex: https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath + !url.pathname.match(/\/\-\/blob\//) + ) { + throw new Error('Please provide full path to yaml file from Gitlab'); + } + try { + const repo = url.pathname.split('/-/blob/')[0]; + + // Find ProjectID from url + // convert 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/filepath' + // to 'https://gitlab.com/api/v4/projects/groupA%2Fteams%2FsubgroupA%2FteamA%2Frepo' + const repoIDLookup = new URL( + `${url.protocol + url.hostname}/api/v4/projects/${encodeURIComponent( + repo.replace(/^\//, ''), + )}`, + ); + const response = await fetch( + repoIDLookup.toString(), + this.getRequestOptions(), + ); + const projectIDJson = await response.json(); + const projectID: Number = projectIDJson.id; + + return projectID; + } catch (e) { + throw new Error(`Could not get GitLab ProjectID for: ${target}, ${e}`); + } + } + + private getRequestOptions(): RequestInit { + return { + headers: { + ['PRIVATE-TOKEN']: this.options.token ?? '', + }, + }; + } + + toString() { + const { host, token } = this.options; + return `gitlab{host=${host},authed=${Boolean(token)}}`; + } +} diff --git a/packages/backend-common/src/reading/UrlReaderPredicateMux.ts b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts new file mode 100644 index 0000000000..7654cc8aac --- /dev/null +++ b/packages/backend-common/src/reading/UrlReaderPredicateMux.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { UrlReader, UrlReaderPredicateTuple } from './types'; + +type Options = { + // UrlReader to fall back to if no other reader is matched + fallback?: UrlReader; +}; + +/** + * A UrlReader implementation that selects from a set of UrlReaders + * based on a predicate tied to each reader. + */ +export class UrlReaderPredicateMux implements UrlReader { + private readonly readers: UrlReaderPredicateTuple[] = []; + private readonly fallback?: UrlReader; + + constructor({ fallback }: Options) { + this.fallback = fallback; + } + + register(tuple: UrlReaderPredicateTuple): void { + this.readers.push(tuple); + } + + read(url: string): Promise { + const parsed = new URL(url); + + for (const { predicate, reader } of this.readers) { + if (predicate(parsed)) { + return reader.read(url); + } + } + + if (this.fallback) { + return this.fallback.read(url); + } + + throw new Error(`No reader found that could handle '${url}'`); + } + + toString() { + return `predicateMux{readers=${this.readers + .map(t => t.reader) + .join(',')},fallback=${this.fallback}}`; + } +} diff --git a/packages/backend-common/src/reading/UrlReaders.ts b/packages/backend-common/src/reading/UrlReaders.ts new file mode 100644 index 0000000000..e1d99a2c49 --- /dev/null +++ b/packages/backend-common/src/reading/UrlReaders.ts @@ -0,0 +1,84 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Logger } from 'winston'; +import { Config } from '@backstage/config'; +import { ReaderFactory, UrlReader } from './types'; +import { UrlReaderPredicateMux } from './UrlReaderPredicateMux'; +import { AzureUrlReader } from './AzureUrlReader'; +import { BitbucketUrlReader } from './BitbucketUrlReader'; +import { GithubUrlReader } from './GithubUrlReader'; +import { GitlabUrlReader } from './GitlabUrlReader'; +import { FetchUrlReader } from './FetchUrlReader'; + +type CreateOptions = { + /** Root config object */ + config: Config; + /** Logger used by all the readers */ + logger: Logger; + /** A list of factories used to construct individual readers that match on URLs */ + factories?: ReaderFactory[]; + /** Fallback reader to use if none of the readers created by the factories match */ + fallback?: UrlReader; +}; + +/** + * UrlReaders provide various utilities related to the UrlReader interface. + */ +export class UrlReaders { + /** + * Creates a UrlReader without any known types. + */ + static create({ + logger, + config, + factories, + fallback, + }: CreateOptions): UrlReader { + const mux = new UrlReaderPredicateMux({ fallback: fallback }); + + for (const factory of factories ?? []) { + const tuples = factory({ config, logger: logger }); + + for (const tuple of tuples) { + mux.register(tuple); + } + } + + return mux; + } + + /** + * Creates a UrlReader that includes all the default factories from this package. + * + * Any additional factories passed will be loaded before the default ones. + * + * If no fallback reader is passed, a plain fetch reader will be used. + */ + static default({ logger, config, factories = [], fallback }: CreateOptions) { + return UrlReaders.create({ + logger, + config, + factories: factories.concat([ + AzureUrlReader.factory, + BitbucketUrlReader.factory, + GithubUrlReader.factory, + GitlabUrlReader.factory, + ]), + fallback: fallback ?? new FetchUrlReader(), + }); + } +} diff --git a/packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx b/packages/backend-common/src/reading/index.ts similarity index 62% rename from packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx rename to packages/backend-common/src/reading/index.ts index 5dad178ccd..8ecc08ebca 100644 --- a/packages/core/src/layout/Sidebar/Settings/AppSettingsList.tsx +++ b/packages/backend-common/src/reading/index.ts @@ -13,14 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { List, ListSubheader } from '@material-ui/core'; -import { SidebarThemeToggle } from './ThemeToggle'; -import { SidebarPinButton } from './PinButton'; -export const AppSettingsList = () => ( - App Settings}> - - - -); +export type { UrlReader } from './types'; +export { UrlReaders } from './UrlReaders'; +export { AzureUrlReader } from './AzureUrlReader'; +export { BitbucketUrlReader } from './BitbucketUrlReader'; +export { GithubUrlReader } from './GithubUrlReader'; +export { GitlabUrlReader } from './GitlabUrlReader'; diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts new file mode 100644 index 0000000000..e423db3eca --- /dev/null +++ b/packages/backend-common/src/reading/types.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Logger } from 'winston'; +import { Config } from '@backstage/config'; + +/** + * A generic interface for fetching plain data from URLs. + */ +export type UrlReader = { + read(url: string): Promise; +}; + +export type UrlReaderPredicateTuple = { + predicate: (url: URL) => boolean; + reader: UrlReader; +}; + +/** + * A factory function that can read config to construct zero or more + * UrlReaders along with a predicate for when it should be used. + */ +export type ReaderFactory = (options: { + config: Config; + logger: Logger; +}) => UrlReaderPredicateTuple[]; diff --git a/packages/backend-common/src/setupTests.ts b/packages/backend-common/src/setupTests.ts index f7b6ca962d..ba33cf996b 100644 --- a/packages/backend-common/src/setupTests.ts +++ b/packages/backend-common/src/setupTests.ts @@ -14,6 +14,4 @@ * limitations under the License. */ -require('jest-fetch-mock').enableMocks(); - export {}; diff --git a/packages/backend/package.json b/packages/backend/package.json index 0941af248a..c3a912c93e 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.1.1-alpha.23", + "version": "0.1.1-alpha.24", "main": "dist/index.cjs.js", "types": "src/index.ts", "private": true, @@ -18,24 +18,24 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.23", - "@backstage/catalog-model": "^0.1.1-alpha.23", - "@backstage/config": "^0.1.1-alpha.23", - "@backstage/plugin-app-backend": "^0.1.1-alpha.23", - "@backstage/plugin-auth-backend": "^0.1.1-alpha.23", - "@backstage/plugin-catalog-backend": "^0.1.1-alpha.23", - "@backstage/plugin-graphql-backend": "^0.1.1-alpha.23", - "@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.23", - "@backstage/plugin-proxy-backend": "^0.1.1-alpha.23", - "@backstage/plugin-rollbar-backend": "^0.1.1-alpha.23", - "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.23", - "@backstage/plugin-sentry-backend": "^0.1.1-alpha.23", - "@backstage/plugin-techdocs-backend": "^0.1.1-alpha.23", + "@backstage/backend-common": "^0.1.1-alpha.24", + "@backstage/catalog-model": "^0.1.1-alpha.24", + "@backstage/config": "^0.1.1-alpha.24", + "@backstage/plugin-app-backend": "^0.1.1-alpha.24", + "@backstage/plugin-auth-backend": "^0.1.1-alpha.24", + "@backstage/plugin-catalog-backend": "^0.1.1-alpha.24", + "@backstage/plugin-graphql-backend": "^0.1.1-alpha.24", + "@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.24", + "@backstage/plugin-proxy-backend": "^0.1.1-alpha.24", + "@backstage/plugin-rollbar-backend": "^0.1.1-alpha.24", + "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.24", + "@backstage/plugin-sentry-backend": "^0.1.1-alpha.24", + "@backstage/plugin-techdocs-backend": "^0.1.1-alpha.24", "@gitbeaker/node": "^23.5.0", "@octokit/rest": "^18.0.0", "azure-devops-node-api": "^10.1.1", "dockerode": "^3.2.0", - "example-app": "^0.1.1-alpha.23", + "example-app": "^0.1.1-alpha.24", "express": "^4.17.1", "express-promise-router": "^3.0.3", "knex": "^0.21.1", @@ -45,7 +45,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/cli": "^0.1.1-alpha.24", "@types/dockerode": "^2.5.32", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index e86034dd29..18fd44f089 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -24,16 +24,16 @@ import Router from 'express-promise-router'; import { - ensureDatabaseExists, - createDatabaseClient, createServiceBuilder, loadBackendConfig, getRootLogger, useHotMemoize, notFoundHandler, + SingleConnectionDatabaseManager, SingleHostDiscovery, + UrlReaders, } from '@backstage/backend-common'; -import { ConfigReader, AppConfig } from '@backstage/config'; +import { ConfigReader } from '@backstage/config'; import healthcheck from './plugins/healthcheck'; import auth from './plugins/auth'; import catalog from './plugins/catalog'; @@ -47,33 +47,26 @@ import graphql from './plugins/graphql'; import app from './plugins/app'; import { PluginEnvironment } from './types'; -function makeCreateEnv(loadedConfigs: AppConfig[]) { - const config = ConfigReader.fromConfigs(loadedConfigs); +function makeCreateEnv(config: ConfigReader) { + const root = getRootLogger(); + const reader = UrlReaders.default({ logger: root, config }); + const discovery = SingleHostDiscovery.fromConfig(config); + + root.info(`Created UrlReader ${reader}`); + + const databaseManager = SingleConnectionDatabaseManager.fromConfig(config); return (plugin: string): PluginEnvironment => { - const logger = getRootLogger().child({ type: 'plugin', plugin }); - const database = createDatabaseClient( - config.getConfig('backend.database'), - { - connection: { - database: `backstage_plugin_${plugin}`, - }, - }, - ); - const discovery = SingleHostDiscovery.fromConfig(config); - return { logger, database, config, discovery }; + const logger = root.child({ type: 'plugin', plugin }); + const database = databaseManager.forPlugin(plugin); + return { logger, database, config, reader, discovery }; }; } async function main() { const configs = await loadBackendConfig(); const configReader = ConfigReader.fromConfigs(configs); - const createEnv = makeCreateEnv(configs); - await ensureDatabaseExists( - configReader.getConfig('backend.database'), - 'backstage_plugin_catalog', - 'backstage_plugin_auth', - ); + const createEnv = makeCreateEnv(configReader); const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck')); const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index 219b04a0f7..b8e99e6398 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -28,12 +28,15 @@ import { useHotCleanup } from '@backstage/backend-common'; export default async function createPlugin({ logger, - database, config, + reader, + database, }: PluginEnvironment) { - const locationReader = new LocationReaders({ logger, config }); + const locationReader = new LocationReaders({ logger, reader, config }); - const db = await DatabaseManager.createDatabase(database, { logger }); + const db = await DatabaseManager.createDatabase(await database.getClient(), { + logger, + }); const entitiesCatalog = new DatabaseEntitiesCatalog(db); const locationsCatalog = new DatabaseLocationsCatalog(db); const higherOrderOperation = new HigherOrderOperations( diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index c5b36ab8da..90667bfb16 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -47,13 +47,12 @@ export default async function createPlugin({ templaters.register('cra', craTemplater); const filePreparer = new FilePreparer(); - const githubPreparer = new GithubPreparer(); + const gitlabPreparer = new GitlabPreparer(config); const azurePreparer = new AzurePreparer(config); const preparers = new Preparers(); preparers.register('file', filePreparer); - preparers.register('github', githubPreparer); preparers.register('gitlab', gitlabPreparer); preparers.register('gitlab/api', gitlabPreparer); preparers.register('azure/api', azurePreparer); @@ -75,6 +74,10 @@ export default async function createPlugin({ token: githubToken, repoVisibility, }); + + const githubPreparer = new GithubPreparer({ token: githubToken }); + + preparers.register('github', githubPreparer); publishers.register('file', githubPublisher); publishers.register('github', githubPublisher); } catch (e) { diff --git a/packages/backend/src/plugins/techdocs.ts b/packages/backend/src/plugins/techdocs.ts index e04d2a2732..6e36c26f39 100644 --- a/packages/backend/src/plugins/techdocs.ts +++ b/packages/backend/src/plugins/techdocs.ts @@ -23,6 +23,7 @@ import { TechdocsGenerator, GithubPreparer, GitlabPreparer, + AzurePreparer, } from '@backstage/plugin-techdocs-backend'; import { PluginEnvironment } from '../types'; import Docker from 'dockerode'; @@ -39,10 +40,12 @@ export default async function createPlugin({ const preparers = new Preparers(); const githubPreparer = new GithubPreparer(logger); const gitlabPreparer = new GitlabPreparer(logger); + const azurePreparer = new AzurePreparer(logger); const directoryPreparer = new DirectoryPreparer(logger); preparers.register('dir', directoryPreparer); preparers.register('github', githubPreparer); preparers.register('gitlab', gitlabPreparer); + preparers.register('azure/api', azurePreparer); const publisher = new LocalPublish(logger); diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts index 3709fc8d9a..f63b9dd780 100644 --- a/packages/backend/src/types.ts +++ b/packages/backend/src/types.ts @@ -14,14 +14,18 @@ * limitations under the License. */ -import Knex from 'knex'; import { Logger } from 'winston'; import { Config } from '@backstage/config'; -import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { + PluginDatabaseManager, + PluginEndpointDiscovery, + UrlReader, +} from '@backstage/backend-common'; export type PluginEnvironment = { logger: Logger; - database: Knex; + database: PluginDatabaseManager; config: Config; + reader: UrlReader; discovery: PluginEndpointDiscovery; }; diff --git a/packages/catalog-model/examples/spotify-api.yaml b/packages/catalog-model/examples/spotify-api.yaml index 30524dfdd4..14deb17abe 100644 --- a/packages/catalog-model/examples/spotify-api.yaml +++ b/packages/catalog-model/examples/spotify-api.yaml @@ -7,8 +7,11 @@ metadata: - spotify - rest annotations: + # The annotation is deprecated, we use placeholders (see below) instead, remove it later. backstage.io/definition-at-location: 'url:https://raw.githubusercontent.com/APIs-guru/openapi-directory/master/APIs/spotify.com/v1/swagger.yaml' spec: type: openapi lifecycle: production owner: spotify@example.com + definition: + $text: https://github.com/APIs-guru/openapi-directory/blob/master/APIs/spotify.com/v1/swagger.yaml diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 96433a0f0e..6a543f7f84 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.1.1-alpha.23", + "version": "0.1.1-alpha.24", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.24", "@types/json-schema": "^7.0.5", "@types/yup": "^0.28.2", "json-schema": "^0.2.5", @@ -29,7 +29,7 @@ "yup": "^0.29.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/cli": "^0.1.1-alpha.24", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/catalog-model/src/entity/ref.test.ts b/packages/catalog-model/src/entity/ref.test.ts index d9787fe675..bd7cc9477d 100644 --- a/packages/catalog-model/src/entity/ref.test.ts +++ b/packages/catalog-model/src/entity/ref.test.ts @@ -15,6 +15,7 @@ */ import { ENTITY_DEFAULT_NAMESPACE } from './constants'; +import { Entity } from './Entity'; import { parseEntityName, parseEntityRef, serializeEntityRef } from './ref'; describe('ref', () => { @@ -333,6 +334,26 @@ describe('ref', () => { expect(serializeEntityRef({ name: 'c' })).toEqual('c'); }); + it('handles entities', () => { + const entityWithNamespace: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'c', + namespace: 'd', + }, + }; + const entityWithoutNamespace: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'c', + }, + }; + expect(serializeEntityRef(entityWithNamespace)).toEqual('b:d/c'); + expect(serializeEntityRef(entityWithoutNamespace)).toEqual('b:c'); + }); + it('picks the least complex form', () => { expect( serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c' }), diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts index c4c86b176c..2cacfce53a 100644 --- a/packages/catalog-model/src/entity/ref.ts +++ b/packages/catalog-model/src/entity/ref.ts @@ -160,12 +160,29 @@ export function parseEntityRef( * @param ref The reference to serialize * @returns The same reference on either string or compound form */ -export function serializeEntityRef(ref: { - kind?: string; - namespace?: string; - name: string; -}): EntityRef { - const { kind, namespace, name } = ref; +export function serializeEntityRef( + ref: + | Entity + | { + kind?: string; + namespace?: string; + name: string; + }, +): EntityRef { + let kind; + let namespace; + let name; + + if ('metadata' in ref) { + kind = ref.kind; + namespace = ref.metadata.namespace; + name = ref.metadata.name; + } else { + kind = ref.kind; + namespace = ref.namespace; + name = ref.name; + } + if ( kind?.includes(':') || kind?.includes('/') || diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts index 200e90b406..457ef64611 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts @@ -161,18 +161,4 @@ describe('CommonValidatorFunctions', () => { ])(`isValidDnsLabel %p ? %p`, (value, result) => { expect(CommonValidatorFunctions.isValidDnsLabel(value)).toBe(result); }); - - it.each([ - ['', ''], - ['a', 'a'], - ['a-b', 'ab'], - ['-a-b', 'ab'], - ['a_b', 'ab'], - [`${'a'.repeat(6000)}`, `${'a'.repeat(6000)}`], - ['_:;>!"#€', ''], - ])(`normalizeToLowercaseAlphanum %p ? %p`, (value, result) => { - expect(CommonValidatorFunctions.normalizeToLowercaseAlphanum(value)).toBe( - result, - ); - }); }); diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts index 96a91aca06..349d6d7f72 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.ts @@ -92,17 +92,4 @@ export class CommonValidatorFunctions { /^[a-z0-9]+(\-[a-z0-9]+)*$/.test(value) ); } - - /** - * Normalizes by keeping only a-z, A-Z, and 0-9; and converts to lowercase. - * - * @param value The value to normalize - */ - static normalizeToLowercaseAlphanum(value: string): string { - return value - .split('') - .filter(x => /[a-zA-Z0-9]/.test(x)) - .join('') - .toLowerCase(); - } } diff --git a/packages/catalog-model/src/validation/makeValidator.ts b/packages/catalog-model/src/validation/makeValidator.ts index 3602c01a63..63341f5c33 100644 --- a/packages/catalog-model/src/validation/makeValidator.ts +++ b/packages/catalog-model/src/validation/makeValidator.ts @@ -23,7 +23,6 @@ const defaultValidators: Validators = { isValidKind: KubernetesValidatorFunctions.isValidKind, isValidEntityName: KubernetesValidatorFunctions.isValidObjectName, isValidNamespace: KubernetesValidatorFunctions.isValidNamespace, - normalizeEntityName: CommonValidatorFunctions.normalizeToLowercaseAlphanum, isValidLabelKey: KubernetesValidatorFunctions.isValidLabelKey, isValidLabelValue: KubernetesValidatorFunctions.isValidLabelValue, isValidAnnotationKey: KubernetesValidatorFunctions.isValidAnnotationKey, diff --git a/packages/catalog-model/src/validation/types.ts b/packages/catalog-model/src/validation/types.ts index ff00036991..14706485ca 100644 --- a/packages/catalog-model/src/validation/types.ts +++ b/packages/catalog-model/src/validation/types.ts @@ -19,7 +19,6 @@ export type Validators = { isValidKind(value: any): boolean; isValidEntityName(value: any): boolean; isValidNamespace(value: any): boolean; - normalizeEntityName(value: string): string; isValidLabelKey(value: any): boolean; isValidLabelValue(value: any): boolean; isValidAnnotationKey(value: any): boolean; diff --git a/packages/cli-common/package.json b/packages/cli-common/package.json index bbd9a67387..abb4b5f9a1 100644 --- a/packages/cli-common/package.json +++ b/packages/cli-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli-common", "description": "Common functionality used by cli, backend, and create-app", - "version": "0.1.1-alpha.23", + "version": "0.1.1-alpha.24", "private": false, "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/cli-common/src/paths.test.ts b/packages/cli-common/src/paths.test.ts index 087f95af38..2278ee3581 100644 --- a/packages/cli-common/src/paths.test.ts +++ b/packages/cli-common/src/paths.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +/* eslint-disable no-restricted-syntax */ import { resolve as resolvePath } from 'path'; import { findPaths, findRootPath, findOwnDir, findOwnRootDir } from './paths'; diff --git a/packages/cli/bin/backstage-cli b/packages/cli/bin/backstage-cli index a065b8ac5a..5c127af2ec 100755 --- a/packages/cli/bin/backstage-cli +++ b/packages/cli/bin/backstage-cli @@ -18,6 +18,7 @@ const path = require('path'); // Figure out whether we're running inside the backstage repo or as an installed dependency +/* eslint-disable-next-line no-restricted-syntax */ const isLocal = require('fs').existsSync(path.resolve(__dirname, '../src')); if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) { @@ -25,6 +26,7 @@ if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) { } else { require('ts-node').register({ transpileOnly: true, + /* eslint-disable-next-line no-restricted-syntax */ project: path.resolve(__dirname, '../../../tsconfig.json'), compilerOptions: { module: 'CommonJS', diff --git a/packages/cli/config/eslint.backend.js b/packages/cli/config/eslint.backend.js index 113ba55818..2c0b05c196 100644 --- a/packages/cli/config/eslint.backend.js +++ b/packages/cli/config/eslint.backend.js @@ -67,6 +67,11 @@ module.exports = { selector: 'ImportDeclaration[source.value="winston"] ImportDefaultSpecifier', }, + { + message: + "`__dirname` doesn't refer to the same dir in production builds, try `resolvePackagePath()` from `@backstage/backend-common` instead.", + selector: 'Identifier[name="__dirname"]', + }, ], }, overrides: [ diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index f69e18190d..b9f709241c 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -96,7 +96,7 @@ async function getConfig() { // Default behaviour is to not apply transforms for node_modules, but we still want // to apply the esm-transformer to .esm.js files, since that's what we use in backstage packages. transformIgnorePatterns: [ - `/node_modules/${transformModulePattern}(?:(?!\\.esm).)*\\.(?:js|json)$`, + `/node_modules/${transformModulePattern}.*\\.(?:(? { program.on('command:*', () => { console.log(); - console.log( - chalk.red(`Invalid command: ${chalk.cyan(program.args.join(' '))}`), - ); - console.log(chalk.red('See --help for a list of available commands.')); + console.log(chalk.red(`Invalid command: ${program.args.join(' ')}`)); console.log(); + program.outputHelp(); process.exit(1); }); - if (!process.argv.slice(2).length) { - program.outputHelp(chalk.yellow); - } - program.parse(argv); }; diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index 2566460494..2e7847fdb1 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -64,6 +64,7 @@ export const makeConfigs = async ( entryFileNames: 'index.cjs.js', chunkFileNames: 'cjs/[name]-[hash].js', format: 'commonjs', + sourcemap: true, }); } if (options.outputs.has(Output.esm)) { @@ -72,6 +73,7 @@ export const makeConfigs = async ( entryFileNames: 'index.esm.js', chunkFileNames: 'esm/[name]-[hash].js', format: 'module', + sourcemap: true, }); // Assume we're building for the browser if ESM output is included mainFields.unshift('browser'); diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 118cb1b40c..00a555eebe 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -209,6 +209,7 @@ export async function createBackendConfig( ], target: 'node' as const, node: { + /* eslint-disable-next-line no-restricted-syntax */ __dirname: true, __filename: true, global: true, diff --git a/packages/cli/src/lib/paths.ts b/packages/cli/src/lib/paths.ts index 06c8f2ab38..a17034344d 100644 --- a/packages/cli/src/lib/paths.ts +++ b/packages/cli/src/lib/paths.ts @@ -16,4 +16,5 @@ import { findPaths } from '@backstage/cli-common'; +/* eslint-disable-next-line no-restricted-syntax */ export const paths = findPaths(__dirname); diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 09f8da3b70..6a17eba76c 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.1.1-alpha.23", + "version": "0.1.1-alpha.24", "private": false, "publishConfig": { "access": "public", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.24", "fs-extra": "^9.0.0", "yaml": "^1.9.2", "yup": "^0.29.1" diff --git a/packages/config-loader/src/lib/reader.test.ts b/packages/config-loader/src/lib/reader.test.ts index 238a556061..6ccceef60d 100644 --- a/packages/config-loader/src/lib/reader.test.ts +++ b/packages/config-loader/src/lib/reader.test.ts @@ -84,6 +84,47 @@ describe('readConfigFile', () => { }); it('should read secrets', async () => { + const readFile = memoryFiles({ + './app-config.yaml': 'app: { $file: "./my-secret" }', + }); + const readSecret = jest.fn().mockResolvedValue('secret'); + + const config = readConfigFile('./app-config.yaml', { + ...mockContext, + readFile, + readSecret: readSecret as ReadSecretFunc, + }); + + await expect(config).resolves.toEqual({ + data: { + app: 'secret', + }, + context: 'app-config.yaml', + }); + expect(readSecret).toHaveBeenCalledWith('.app', { + file: './my-secret', + }); + }); + + it('should not allow keys adjacent to secrets', async () => { + const readFile = memoryFiles({ + './app-config.yaml': 'app: { extraKey: 3, $file: "./my-secret" }', + }); + const readSecret = jest.fn().mockResolvedValue('secret'); + + const config = readConfigFile('./app-config.yaml', { + ...mockContext, + readFile, + readSecret: readSecret as ReadSecretFunc, + }); + + await expect(config).rejects.toThrow( + "Secret key '$file' has adjacent keys at .app", + ); + expect(readSecret).not.toHaveBeenCalled(); + }); + + it('should read deprecated secrets', async () => { const readFile = memoryFiles({ './app-config.yaml': 'app: { $secret: { file: "./my-secret" } }', }); @@ -106,7 +147,7 @@ describe('readConfigFile', () => { }); }); - it('should require secrets to be objects', async () => { + it('should require deprecated secrets to be objects', async () => { const readFile = memoryFiles({ './app-config.yaml': 'app: { $secret: ["wrong-type"] }', }); diff --git a/packages/config-loader/src/lib/reader.ts b/packages/config-loader/src/lib/reader.ts index e2c9bebdf3..8eadae0fe0 100644 --- a/packages/config-loader/src/lib/reader.ts +++ b/packages/config-loader/src/lib/reader.ts @@ -31,6 +31,8 @@ export async function readConfigFile( const configYaml = await ctx.readFile(filePath); const config = yaml.parse(configYaml); + const context = basename(filePath); + async function transform( obj: JsonValue, path: string, @@ -56,7 +58,11 @@ export async function readConfigFile( return arr; } + // TODO(Rugvip): This form of declaring secrets is deprecated, warn and remove in the future if ('$secret' in obj) { + console.warn( + `Deprecated secret declaration at '${path}' in '${context}', use $env, $file, etc. instead`, + ); if (!isObject(obj.$secret)) { throw TypeError(`Expected object at secret ${path}.$secret`); } @@ -68,6 +74,24 @@ export async function readConfigFile( } } + // Check if there's any key that starts with a '$', in that case we treat + // this entire object as a secret. + const [secretKey] = Object.keys(obj).filter(key => key.startsWith('$')); + if (secretKey) { + if (Object.keys(obj).length !== 1) { + throw new Error( + `Secret key '${secretKey}' has adjacent keys at ${path}`, + ); + } + try { + return await ctx.readSecret(path, { + [secretKey.slice(1)]: obj[secretKey], + }); + } catch (error) { + throw new Error(`Invalid secret at ${path}: ${error.message}`); + } + } + const out: JsonObject = {}; for (const [key, value] of Object.entries(obj)) { @@ -87,5 +111,5 @@ export async function readConfigFile( if (!isObject(finalConfig)) { throw new TypeError('Expected object at config root'); } - return { data: finalConfig, context: basename(filePath) }; + return { data: finalConfig, context }; } diff --git a/packages/config-loader/src/lib/secrets.test.ts b/packages/config-loader/src/lib/secrets.test.ts index fd95e527af..cfc4150d68 100644 --- a/packages/config-loader/src/lib/secrets.test.ts +++ b/packages/config-loader/src/lib/secrets.test.ts @@ -56,21 +56,33 @@ describe('readSecret', () => { }); it('should read data secrets', async () => { + // Deprecated object form await expect( readSecret({ data: 'my-data.json', path: 'a.b.c' }, ctx), ).resolves.toBe('42'); - await expect( readSecret({ data: 'my-data.yaml', path: 'some.yaml.key' }, ctx), ).resolves.toBe('7'); - await expect( readSecret({ data: 'my-data.yml', path: 'different.key' }, ctx), ).resolves.toBe('hello'); - await expect( readSecret({ data: 'no-data.yml', path: 'different.key' }, ctx), ).rejects.toThrow('File not found!'); + + // New format with path in fragment + await expect(readSecret({ data: 'my-data.json#a.b.c' }, ctx)).resolves.toBe( + '42', + ); + await expect( + readSecret({ data: 'my-data.yaml#some.yaml.key' }, ctx), + ).resolves.toBe('7'); + await expect( + readSecret({ data: 'my-data.yml#different.key' }, ctx), + ).resolves.toBe('hello'); + await expect( + readSecret({ data: 'no-data.yml#different.key' }, ctx), + ).rejects.toThrow('File not found!'); }); it('should reject invalid secrets', async () => { @@ -84,7 +96,7 @@ describe('readSecret', () => { "Secret must contain one of 'file', 'env', 'data'", ); await expect(readSecret({ data: 'no-data.yml' }, ctx)).rejects.toThrow( - 'path is a required field', + "Invalid format for data secret value, must be of the form #, got 'no-data.yml'", ); await expect( readSecret({ data: 'no-parser.js', path: '.' }, ctx), diff --git a/packages/config-loader/src/lib/secrets.ts b/packages/config-loader/src/lib/secrets.ts index 348a41db58..4c3812d527 100644 --- a/packages/config-loader/src/lib/secrets.ts +++ b/packages/config-loader/src/lib/secrets.ts @@ -38,9 +38,8 @@ type EnvSecret = { type DataSecret = { // Path to the data secret file, relative to the config file. data: string; - // The path to the value inside the data file. - // Either a '.' separated list, or an array of path segments. - path: string | string[]; + // The path to the value inside the data file, each element separated by '.'. + path?: string; }; type Secret = FileSecret | EnvSecret | DataSecret; @@ -55,12 +54,6 @@ const secretLoaderSchemas = { }), data: yup.object({ data: yup.string().required(), - path: yup.lazy(value => { - if (typeof value === 'string') { - return yup.string().required(); - } - return yup.array().of(yup.string().required()).required(); - }), }), }; @@ -111,24 +104,30 @@ export async function readSecret( return ctx.env[secret.env]; } if ('data' in secret) { - const ext = extname(secret.data); + const url = + 'path' in secret ? `${secret.data}#${secret.path}` : secret.data; + const [filePath, dataPath] = url.split(/#(.*)/); + if (!dataPath) { + throw new Error( + `Invalid format for data secret value, must be of the form #, got '${url}'`, + ); + } + + const ext = extname(filePath); const parser = dataSecretParser[ext]; if (!parser) { throw new Error(`No data secret parser available for extension ${ext}`); } - const content = await ctx.readFile(secret.data); + const content = await ctx.readFile(filePath); - const { path } = secret; - const parts = typeof path === 'string' ? path.split('.') : path; + const parts = dataPath.split('.'); let value: JsonValue | undefined = await parser(content); for (const [index, part] of parts.entries()) { if (!isObject(value)) { const errPath = parts.slice(0, index).join('.'); - throw new Error( - `Value is not an object at ${errPath} in ${secret.data}`, - ); + throw new Error(`Value is not an object at ${errPath} in ${filePath}`); } value = value[part]; } diff --git a/packages/config-loader/src/loader.test.ts b/packages/config-loader/src/loader.test.ts index a7547ee8fb..087e2309c0 100644 --- a/packages/config-loader/src/loader.test.ts +++ b/packages/config-loader/src/loader.test.ts @@ -24,8 +24,7 @@ describe('loadConfig', () => { app: title: Example App sessionKey: - $secret: - file: secrets/session-key.txt + $file: secrets/session-key.txt `, '/root/app-config.development.yaml': ` app: diff --git a/packages/config/package.json b/packages/config/package.json index 58c86d6e96..d3695ebf33 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "0.1.1-alpha.23", + "version": "0.1.1-alpha.24", "private": false, "publishConfig": { "access": "public", diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 7d338c944d..c925471695 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-api", "description": "Internal Core API used by Backstage plugins and apps", - "version": "0.1.1-alpha.23", + "version": "0.1.1-alpha.24", "private": false, "publishConfig": { "access": "public", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.23", - "@backstage/theme": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.24", + "@backstage/theme": "^0.1.1-alpha.24", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@types/react": "^16.9", @@ -41,8 +41,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.23", - "@backstage/test-utils-core": "^0.1.1-alpha.23", + "@backstage/cli": "^0.1.1-alpha.24", + "@backstage/test-utils-core": "^0.1.1-alpha.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/packages/core-api/src/apis/definitions/AppThemeApi.ts b/packages/core-api/src/apis/definitions/AppThemeApi.ts index de70adabd9..3f580450f0 100644 --- a/packages/core-api/src/apis/definitions/AppThemeApi.ts +++ b/packages/core-api/src/apis/definitions/AppThemeApi.ts @@ -17,6 +17,7 @@ import { createApiRef } from '../ApiRef'; import { BackstageTheme } from '@backstage/theme'; import { Observable } from '../../types'; +import { SvgIconProps } from '@material-ui/core'; /** * Describes a theme provided by the app. @@ -41,6 +42,11 @@ export type AppTheme = { * The specialized MaterialUI theme instance. */ theme: BackstageTheme; + + /** + * An Icon for the theme mode setting. + */ + icon?: React.ReactElement; }; /** diff --git a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts index b6aac75620..dd9657d7d8 100644 --- a/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts +++ b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts @@ -15,11 +15,8 @@ */ import { createApiRef } from '../ApiRef'; -import { - UserFlags, - FeatureFlagsRegistry, - FeatureFlagsRegistryItem, -} from '../../app/FeatureFlags'; +import { UserFlags, FeatureFlagsRegistry } from '../../app/FeatureFlags'; +import { FeatureFlagName } from '../../plugin'; /** * The feature flags API is used to toggle functionality to users across plugins and Backstage. @@ -55,6 +52,11 @@ export interface FeatureFlagsApi { getRegisteredFlags(): FeatureFlagsRegistry; } +export interface FeatureFlagsRegistryItem { + pluginId: string; + name: FeatureFlagName; +} + export const featureFlagsApiRef = createApiRef({ id: 'core.featureflags', description: 'Used to toggle functionality in features across Backstage', diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 9dd2fb138f..c222c3c420 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -30,12 +30,12 @@ import { SignInPageProps, } from './types'; import { BackstagePlugin } from '../plugin'; -import { FeatureFlagsRegistryItem } from './FeatureFlags'; import { featureFlagsApiRef, AppThemeApi, ConfigApi, identityApiRef, + FeatureFlagsRegistryItem, } from '../apis/definitions'; import { AppThemeProvider } from './AppThemeProvider'; diff --git a/packages/core-api/src/app/FeatureFlags.tsx b/packages/core-api/src/app/FeatureFlags.tsx index 3db2a18a02..7a69fa5456 100644 --- a/packages/core-api/src/app/FeatureFlags.tsx +++ b/packages/core-api/src/app/FeatureFlags.tsx @@ -15,7 +15,11 @@ */ import { FeatureFlagName } from '../plugin/types'; -import { FeatureFlagState, FeatureFlagsApi } from '../apis/definitions'; +import { + FeatureFlagState, + FeatureFlagsApi, + FeatureFlagsRegistryItem, +} from '../apis/definitions'; /** * Helper method for validating compatibility and flag name. @@ -129,10 +133,6 @@ export class UserFlags extends Map { * This acts as a holding data structure for feature flags * that plugins wish to register for use in Backstage. */ -export interface FeatureFlagsRegistryItem { - pluginId: string; - name: FeatureFlagName; -} export class FeatureFlagsRegistry extends Array { static from(entries: FeatureFlagsRegistryItem[]) { diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index 882ecd3d8b..8a1eb92e8e 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -112,11 +112,13 @@ export type AppOptions = { * title: 'Light Theme', * variant: 'light', * theme: lightTheme, + * icon: , * }, { * id: 'dark', * title: 'Dark Theme', * variant: 'dark', * theme: darkTheme, + * icon: , * }] * ``` */ diff --git a/packages/core/package.json b/packages/core/package.json index 8749a37b92..4890552c99 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.1.1-alpha.23", + "version": "0.1.1-alpha.24", "private": false, "publishConfig": { "access": "public", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.23", - "@backstage/core-api": "^0.1.1-alpha.23", - "@backstage/theme": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.24", + "@backstage/core-api": "^0.1.1-alpha.24", + "@backstage/theme": "^0.1.1-alpha.24", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -39,8 +39,9 @@ "@types/react-sparklines": "^1.7.0", "classnames": "^2.2.6", "clsx": "^1.1.0", + "immer": "^7.0.9", "lodash": "^4.17.15", - "material-table": "1.68.0", + "material-table": "^1.69.1", "prop-types": "^15.7.2", "rc-progress": "^3.0.0", "react": "^16.12.0", @@ -54,8 +55,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.23", - "@backstage/test-utils": "^0.1.1-alpha.23", + "@backstage/cli": "^0.1.1-alpha.24", + "@backstage/test-utils": "^0.1.1-alpha.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/packages/core/src/api-wrappers/createApp.tsx b/packages/core/src/api-wrappers/createApp.tsx index 7fe77423e2..c7d0f5ce01 100644 --- a/packages/core/src/api-wrappers/createApp.tsx +++ b/packages/core/src/api-wrappers/createApp.tsx @@ -22,7 +22,8 @@ import privateExports, { AppConfigLoader, } from '@backstage/core-api'; import { BrowserRouter, MemoryRouter } from 'react-router-dom'; - +import LightIcon from '@material-ui/icons/WbSunny'; +import DarkIcon from '@material-ui/icons/Brightness2'; import { ErrorPage } from '../layout/ErrorPage'; import { Progress } from '../components/Progress'; import { defaultApis } from './defaultApis'; @@ -110,12 +111,14 @@ export function createApp(options?: AppOptions) { title: 'Light Theme', variant: 'light', theme: lightTheme, + icon: , }, { id: 'dark', title: 'Dark Theme', variant: 'dark', theme: darkTheme, + icon: , }, ]; const configLoader = options?.configLoader ?? defaultConfigLoader; diff --git a/packages/core/src/components/Button/Button.stories.tsx b/packages/core/src/components/Button/Button.stories.tsx index aaca389f82..a5005574fe 100644 --- a/packages/core/src/components/Button/Button.stories.tsx +++ b/packages/core/src/components/Button/Button.stories.tsx @@ -24,7 +24,7 @@ const Location = () => { }; export default { - title: 'Button', + title: 'Inputs/Button', component: Button, decorators: [ (storyFn: FunctionComponentFactory<{}>) => ( diff --git a/packages/core/src/components/CheckboxTree/CheckboxTree.stories.tsx b/packages/core/src/components/CheckboxTree/CheckboxTree.stories.tsx new file mode 100644 index 0000000000..d5a33f0f9b --- /dev/null +++ b/packages/core/src/components/CheckboxTree/CheckboxTree.stories.tsx @@ -0,0 +1,73 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { CheckboxTree } from '.'; + +const CHECKBOX_TREE_ITEMS = [ + { + label: 'Genereic subcategory name 1', + options: [ + { + label: 'Option 1', + value: 1, + }, + { + label: 'Option 2', + value: 2, + }, + ], + }, + { + label: 'Genereic subcategory name 2', + options: [ + { + label: 'Option 1', + value: 1, + }, + { + label: 'Option 2', + value: 2, + }, + ], + }, + { + label: 'Genereic subcategory name 3', + options: [ + { + label: 'Option 1', + value: 1, + }, + { + label: 'Option 2', + value: 2, + }, + ], + }, +]; + +export default { + title: 'Inputs/CheckboxTree', + component: CheckboxTree, +}; + +export const Default = () => ( + {}} + label="default" + subCategories={CHECKBOX_TREE_ITEMS} + /> +); diff --git a/packages/core/src/components/CheckboxTree/CheckboxTree.test.tsx b/packages/core/src/components/CheckboxTree/CheckboxTree.test.tsx new file mode 100644 index 0000000000..f5eaa0e58b --- /dev/null +++ b/packages/core/src/components/CheckboxTree/CheckboxTree.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; + +import { CheckboxTree } from '.'; + +const CHECKBOX_TREE_ITEMS = [ + { + label: 'Genereic subcategory name 1', + options: [ + { + label: 'Option 1', + value: 1, + }, + { + label: 'Option 2', + value: 2, + }, + ], + }, +]; + +const minProps = { + onChange: jest.fn(), + label: 'Default', + subCategories: CHECKBOX_TREE_ITEMS, +}; + +describe('', () => { + it('renders without exploding', async () => { + const { getByText, getByTestId } = render(); + + expect(getByText('Genereic subcategory name 1')).toBeInTheDocument(); + const checkbox = await getByTestId('expandable'); + + // Simulate click on expandable arrow + fireEvent.click(checkbox); + + // Simulate click on option + const option = getByText('Option 1'); + expect(getByText('Option 1')).toBeInTheDocument(); + fireEvent.click(option); + expect(minProps.onChange).toHaveBeenCalled(); + }); +}); diff --git a/packages/core/src/components/CheckboxTree/CheckboxTree.tsx b/packages/core/src/components/CheckboxTree/CheckboxTree.tsx new file mode 100644 index 0000000000..6825704971 --- /dev/null +++ b/packages/core/src/components/CheckboxTree/CheckboxTree.tsx @@ -0,0 +1,294 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/* eslint-disable guard-for-in */ +import React, { useEffect, useReducer } from 'react'; +import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; +import { + List, + ListItem, + ListItemIcon, + Checkbox, + ListItemText, + Collapse, + Typography, +} from '@material-ui/core'; +import ExpandLess from '@material-ui/icons/ExpandLess'; +import ExpandMore from '@material-ui/icons/ExpandMore'; +import produce from 'immer'; + +type IndexedObject = { + [key: string]: T; +}; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + width: '100%', + minWidth: 10, + maxWidth: 360, + backgroundColor: 'transparent', + '&:hover': { + backgroundColor: 'transparent', + }, + '&:active': { + animation: 'none', + transform: 'none', + }, + }, + nested: { + paddingLeft: theme.spacing(5), + height: '32px', + '&:hover': { + backgroundColor: 'transparent', + }, + }, + listItemIcon: { + minWidth: 10, + }, + listItem: { + '&:hover': { + backgroundColor: 'transparent', + }, + }, + text: { + '& span, & svg': { + fontWeight: 'normal', + fontSize: 14, + }, + }, + }), +); + +/* SUB_CATEGORY */ + +type SubCategory = { + label: string; + isChecked?: boolean; + isOpen?: boolean; + options?: Option[]; +}; + +type SubCategoryWithIndexedOptions = { + label: string; + isChecked?: boolean; + isOpen?: boolean; + options: IndexedObject