diff --git a/.codecov.yml b/.codecov.yml index d03ad99836..1b8e770b59 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -13,6 +13,8 @@ flags: core: paths: - packages/core/ + carryforward: true core-api: paths: - packages/core-api/ + carryforward: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 25360f3e1e..d360dd995d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ If you encounter issues while upgrading to a newer version, don't hesitate to re - Renamed `SessionStateApi` to `SessionApi` and `logout` to `signOut`. Custom implementations of the `SingInPage` app-component will need to rename their `logout` function. The different auth provider items for the `UserSettingsMenu` have been consolidated into a single `ProviderSettingsItem`, meaning you need to replace existing usages of `OAuthProviderSettings` and `OIDCProviderSettings`. [#2555](https://github.com/spotify/backstage/pull/2555). +### @backstage/auth-backend + +- The default mount path of backend plugins was changed to `/api/:pluginId`, and as part of that it was needed to enable configuration of the base path of the auth backend, so that it can construct redirect URLs correctly. The default base path is `/api/auth`, but you need to set this to `/auth` if you want to keep using the old path. Note that you will also need to reconfigure any allowed redirect URLs to include `/api` if you switch to the new recommended pattern. [#2562](https://github.com/spotify/backstage/pull/2562) + ## v0.1.1-alpha.22 ### @backstage/core diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a6f7e5e5e1..d3c2780967 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,6 +32,10 @@ If you start developing a plugin that you aim to release as open source, we sugg You can also use this process if you have an idea for a good plugin but you hope that someone else will pick up the work. +## Adding Non-code Contributions + +Since there is such a large landscape of possible development, build, and deployment environments, we welcome community contributions in these areas in the [`/contrib`](https://github.com/spotify/backstage/tree/master/contrib) folder of the project. This is an excellent place to put things that help out the community at large, but which may not fit within the scope of the core product to support natively. Here, you will find Helm charts, alternative Docker images, and much more. + ## Write Documentation The current documentation is very limited. Help us make the `/docs` folder come alive. diff --git a/README.md b/README.md index 9ef09aee06..e05c1eb16e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Out of the box, Backstage includes: For more information go to [backstage.io](https://backstage.io) or join our [Discord chatroom](https://discord.gg/EBHEGzX). +🎉 Backstage is a CNCF Sandbox project. Read the announcement [here](https://backstage.io/blog/2020/09/23/backstage-cncf-sandbox). + ## Project roadmap A detailed project roadmap, including already delivered milestones, is available [here](https://backstage.io/docs/overview/roadmap). @@ -51,11 +53,9 @@ Check out [the documentation](https://backstage.io/docs/getting-started) on how - [Code of Conduct](CODE_OF_CONDUCT.md) - This is how we roll - [Adopters](ADOPTERS.md) - Companies already using Backstage - [Blog](https://backstage.io/blog/) - Announcements and updates -- [Newsletter](https://mailchi.mp/spotify/backstage-community) +- [Newsletter](https://mailchi.mp/spotify/backstage-community) - Subscribe to our email newsletter - Give us a star ⭐️ - If you are using Backstage or think it is an interesting project, we would love a star ❤️ -Or, if you are an open source developer and are interested in joining our team, please reach out to [foss-opportunities@spotify.com ](mailto:foss-opportunities@spotify.com) - ## License Copyright 2020 Spotify AB. diff --git a/app-config.yaml b/app-config.yaml index 60ecb813f7..01925c21dd 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -33,8 +33,8 @@ organization: name: Spotify techdocs: - storageUrl: http://localhost:7000/techdocs/static/docs - requestUrl: http://localhost:7000/techdocs/docs + storageUrl: http://localhost:7000/api/techdocs/static/docs + requestUrl: http://localhost:7000/api/techdocs/docs generators: techdocs: 'docker' @@ -55,6 +55,10 @@ newrelic: lighthouse: baseUrl: http://localhost:3003 +kubernetes: + clusterLocatorMethod: 'configMultiTenant' + clusters: [] + catalog: rules: - allow: [Component, API, Group, Template, Location] diff --git a/docs/architecture-decisions/adr008-default-catalog-file-name.md b/docs/architecture-decisions/adr008-default-catalog-file-name.md index c57d10c0b1..d4716318bc 100644 --- a/docs/architecture-decisions/adr008-default-catalog-file-name.md +++ b/docs/architecture-decisions/adr008-default-catalog-file-name.md @@ -7,7 +7,7 @@ description: Architecture Decision Record (ADR) log on Default Catalog File Name ## Background While the spec for the catalog file format is well described in -[ADR002](./adr002-default-catalog-file-format.md), guidance was note provided as +[ADR002](./adr002-default-catalog-file-format.md), guidance was not provided as to the name of the catalog file. Following discussion in @@ -23,4 +23,4 @@ catalog-info.yaml ``` This name is a default, **not a requirement**. The catalog file will work with -Backstage irregardless of its name. +Backstage regardless of its name. diff --git a/docs/architecture-decisions/adr009-entity-references.md b/docs/architecture-decisions/adr009-entity-references.md new file mode 100644 index 0000000000..8b7984ea49 --- /dev/null +++ b/docs/architecture-decisions/adr009-entity-references.md @@ -0,0 +1,69 @@ +--- +id: adrs-adr009 +title: ADR009: Entity References +description: Architecture Decision Record (ADR) log on Entity References +--- + +## Background + +While the spec for the catalog file format is well described in +[ADR002](./adr002-default-catalog-file-format.md), guidance was not provided as +to how one is expected to express references to other entities in the catalog. +There was also some confusion on how to reference entities in URLs in the +Backstage frontend. + +Following discussion in +[Issue 1947](https://github.com/spotify/backstage/issues/1947), a decision was +made. + +## Entity References in YAML files + +The textual format, as written by humans, to reference entities by name is on +the following form, where square brackets denote optionality: + +``` +[:][/] +``` + +That is, it is composed of between one and three parts in this specific order, +without any additional encoding, with those exact separator characters. +Optionality of `kind` and `namespace` are contextual, and they may or may not +have default contextual fallback values. + +When that format is insufficient or when machine made interchange formats wish +to express such relations in a more expressive form, a nested structure on the +following form can be used: + +```yaml +kind: +namespace: +name: +``` + +Of these, only `name` is always required. Optionality of `kind` and `namespace` +are contextual, and they may or may not have default contextual fallback values. +All other possible key values in this structure are reserved for future use. + +A system or user wanting to express a full entity name that is always valid, +shall supply the entire triplet whether using the string form or the compound +form. + +A full description of the format can be found +[in the documentation](https://backstage.io/docs/features/software-catalog/references). + +## Entity References in URLs + +Where entities are referenced by name in the Backstage frontend, the URL +containing the reference shall take the following form: + +``` +:namespace/:kind/:name +``` + +All three parts are required under all circumstances. The default value for the +`namespace` in the catalog is the string `"default"`, if the entity does not +specify one explicitly in `metadata.namespace`. + +This means that we do not encourage the string form of entity references to be +used as a single URL segment, due to the use of URL-unsafe characters leading to +possible risk, confusion, and uglier URLs. diff --git a/docs/auth/add-auth-provider.md b/docs/auth/add-auth-provider.md index c5f7645a49..56586cbfc6 100644 --- a/docs/auth/add-auth-provider.md +++ b/docs/auth/add-auth-provider.md @@ -229,9 +229,10 @@ name. ### Test the new provider -You can `curl -i localhost:7000/auth/providerA/start` and which should provide a -`302` redirect with a `Location` header. Paste the url from that header into a -web browser and you should be able to trigger the authorization flow. +You can `curl -i localhost:7000/api/auth/providerA/start` and which should +provide a `302` redirect with a `Location` header. Paste the url from that +header into a web browser and you should be able to trigger the authorization +flow. --- diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 301d80630c..0c50e3669a 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -22,6 +22,8 @@ humans. However, the structure and semantics is the same in both cases. - [Kind: Component](#kind-component) - [Kind: Template](#kind-template) - [Kind: API](#kind-api) +- [Kind: Group](#kind-group) +- [Kind: User](#kind-user) ## Overall Shape Of An Entity @@ -571,3 +573,167 @@ group of people in an organizational structure. The definition of the API, based on the format defined by `spec.type`. This field is required. + +## Kind: Group + +Describes the following entity kind: + +| Field | Value | +| ------------ | ----------------------- | +| `apiVersion` | `backstage.io/v1alpha1` | +| `kind` | `Group` | + +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`](#kind-user). + +Descriptor files for this kind may look as follows. + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: Group +metadata: + name: infrastructure + description: The infra business unit +spec: + type: business-unit + parent: ops + ancestors: [ops, global-synergies, acme-corp] + children: [backstage, other] + descendants: [backstage, other, team-a, team-b, team-c, team-d] +``` + +In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata) +shape, this kind has the following structure. + +### `apiVersion` and `kind` [required] + +Exactly equal to `backstage.io/v1alpha1` and `Group`, respectively. + +### `spec.type` [required] + +The type of group as a string, e.g. `team`. There is currently no enforced set +of values for this field, so it is left up to the adopting organization to +choose a nomenclature that matches their org hierarchy. + +Some common values for this field could be: + +- `team` +- `business-unit` +- `product-area` +- `root` - as a common virtual root of the hierarchy, if desired + +### `spec.parent` [optional] + +The immediate parent group in the hierarchy, if any. Not all groups must have a +parent; the catalog supports multi-root hierarchies. Groups may however not have +more than one parent. + +This field is an +[entity reference](https://backstage.io/docs/features/software-catalog/references), +with the default kind `Group` and the default namespace equal to the same +namespace as the user. Only `Group` entities may be referenced. Most commonly, +this field points to a group in the same namespace, so in those cases it is +sufficient to enter only the `metadata.name` field of that group. + +### `spec.ancestors` [required] + +The recursive list of parents up the hierarchy, by stepping through parents one +by one. The list must be present, but may be empty if `parent` is not present. +The first entry in the list is equal to `parent`, and then the following ones +are progressively farther up the hierarchy. + +The entries of this array are +[entity references](https://backstage.io/docs/features/software-catalog/references), +with the default kind `Group` and the default namespace equal to the same +namespace as the user. Only `Group` entities may be referenced. Most commonly, +these entries point to groups in the same namespace, so in those cases it is +sufficient to enter only the `metadata.name` field of those groups. + +### `spec.children` [required] + +The immediate child groups of this group in the hierarchy (whose `parent` field +points to this group). The list must be present, but may be empty if there are +no child groups. The items are not guaranteed to be ordered in any particular +way. + +The entries of this array are +[entity references](https://backstage.io/docs/features/software-catalog/references), +with the default kind `Group` and the default namespace equal to the same +namespace as the user. Only `Group` entities may be referenced. Most commonly, +these entries point to groups in the same namespace, so in those cases it is +sufficient to enter only the `metadata.name` field of those groups. + +### `spec.descendants` [required] + +The immediate and recursive child groups of this group in the hierarchy +(children, and children's children, etc.). The list must be present, but may be +empty if there are no child groups. The items are not guaranteed to be ordered +in any particular way. + +The entries of this array are +[entity references](https://backstage.io/docs/features/software-catalog/references), +with the default kind `Group` and the default namespace equal to the same +namespace as the user. Only `Group` entities may be referenced. Most commonly, +these entries point to groups in the same namespace, so in those cases it is +sufficient to enter only the `metadata.name` field of those groups. + +## Kind: User + +Describes the following entity kind: + +| Field | Value | +| ------------ | ----------------------- | +| `apiVersion` | `backstage.io/v1alpha1` | +| `kind` | `User` | + +A user describes a person, such as an employee, a contractor, or similar. Users +belong to [`Group`](#kind-group) entities in the catalog. + +These catalog user entries are connected to the way that authentication within +the Backstage ecosystem works. See the [auth](https://backstage.io/docs/auth) +section of the docs for a discussion of these concepts. + +Descriptor files for this kind may look as follows. + +```yaml +apiVersion: backstage.io/v1alpha1 +kind: User +metadata: + name: jdoe +spec: + profile: + displayName: Jenny Doe + email: jenny-doe@example.com + picture: https://example.com/staff/jenny-with-party-hat.jpeg + memberOf: [team-b, employees] +``` + +In addition to the [common envelope metadata](#common-to-all-kinds-the-metadata) +shape, this kind has the following structure. + +### `apiVersion` and `kind` [required] + +Exactly equal to `backstage.io/v1alpha1` and `User`, respectively. + +### `spec.profile` [optional] + +Optional profile information about the user, mainly for display purposes. All +fields of this structure are also optional. The email would be a primary email +of some form, that the user may wish to be used for contacting them. The picture +is expected to be a URL pointing to an image that's representative of the user, +and that a browser could fetch and render on a profile page or similar. + +### `spec.memberOf` [required] + +The list of groups that the user is a direct member of (i.e., no transitive +memberships are listed here). The list must be present, but may be empty if the +user is not member of any groups. The items are not guaranteed to be ordered in +any particular way. + +The entries of this array are +[entity references](https://backstage.io/docs/features/software-catalog/references), +with the default kind `Group` and the default namespace equal to the same +namespace as the user. Only `Group` entities may be referenced. Most commonly, +these entries point to groups in the same namespace, so in those cases it is +sufficient to enter only the `metadata.name` field of those groups. diff --git a/docs/features/software-catalog/references.md b/docs/features/software-catalog/references.md new file mode 100644 index 0000000000..5093ecc49e --- /dev/null +++ b/docs/features/software-catalog/references.md @@ -0,0 +1,104 @@ +--- +id: references +title: Entity References +description: How to express references between entities +--- + +Entities commonly have a need to reference other entities. For example, a +[Component](descriptor-format.md#kind-component) entity may want to declare who +its owner is by mentioning a Group or User entity, and a User entity may want to +declare what Group entities it is a member of. This article describes how to +write those references in your yaml entity declaration files. + +Each entity in the catalog is uniquely identified by the triplet of its +[kind](descriptor-format.md#apiversion-and-kind-required), +[namespace](descriptor-format.md#namespace-optional), and +[name](descriptor-format.md#name-required). But that's a lot to type out +manually, and in a lot of circumstances, both the kind and the namespace are +fixed, or possible to deduce, or could have sane default values. So in order to +help the writer, the catalog has a few tricks up its sleeve. + +Each reference can be expressed in one of two ways: as a compact string, or as a +compound reference structure. + +## String References + +This is the most common alternative, that should be used in almost all +circumstances. + +The string is on the form `[:][/]`, that is, it is +composed of between one and three parts in this specific order, without any +additional encoding: + +- Optionally, the kind, followed by a colon +- Optionally, the namespace, followed by a forward slash +- The name + +The name is always required. Depending on the context, you may be able to leave +out the kind and/or namespace. If you do, it is contextual what values will be +used, and the relevant documentation should specify which rule applies where. +All strings are case insensitive. + +```yaml +# Example: +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: petstore + namespace: external-systems + description: Petstore +spec: + type: service + lifecycle: experimental + owner: group:pet-managers + implementsApis: + - petstore + - internal/streetlights + - hello-world +``` + +The field `spec.owner` is a reference. In this case, the string +`group:pet-managers` was given by the user. That means that the kind is `Group`, +the namespace is left out, and the name is `pet-managers`. In this context, the +namespace was chosen to fall back to the value `default` by the code that parsed +the reference, so the end result is that we expect to find another entity in the +catalog that is of kind `Group`, namespace `default` (which, actually, also can +be left out in its own yaml file because that's the default value there too), +and name `pet-managers`. + +The entries in `implementsApis` are also references. In this case, none of them +need to specify a kind since we know from the context that that's the only kind +that's supported here. The second entry specifies a namespace but the other ones +don't, and in this context, the default is to refer to the same namespace as the +originating entity (`external-systems` here). So the three references +essentially expand to `api:external-systems/petstore`, +`api:internal/streetlights`, and `api:external-systems/hello-world`. We expect +there to exist three API kind entities in the catalog matching those references. + +## Compound References + +This is a more verbose version of a reference, where each part of the +kind-namespace-name triplet is expressed as a field in a structure. This format +can be used where necessary, such as if either of the three elements contain +colons or forward slashes. Avoid using it where possible, since it is harder to +read and write for humans. + +```yaml +# Example: +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: petstore + description: Petstore +spec: + type: service + lifecycle: experimental + owner: + kind: Group + name: aegis-imports/pet-managers +``` + +In this example, the `spec.owner` has been broken apart since the name was +complex. The kind happened to be written with an uppercase letter G, which also +works. The namespace was left out just like in the string version above, which +is handled identically. diff --git a/docs/features/software-templates/adding-templates.md b/docs/features/software-templates/adding-templates.md index fad5c68e41..59019e754e 100644 --- a/docs/features/software-templates/adding-templates.md +++ b/docs/features/software-templates/adding-templates.md @@ -88,7 +88,7 @@ running: ```sh curl \ --location \ - --request POST 'localhost:7000/catalog/locations' \ + --request POST 'localhost:7000/api/catalog/locations' \ --header 'Content-Type: application/json' \ --data-raw "{\"type\": \"file\", \"target\": \"${YOUR PATH HERE}/template.yaml\"}" ``` @@ -98,7 +98,7 @@ If loading from a Git location, you can run the following ```sh curl \ --location \ - --request POST 'localhost:7000/catalog/locations' \ + --request POST 'localhost:7000/api/catalog/locations' \ --header 'Content-Type: application/json' \ --data-raw "{\"type\": \"github\", \"target\": \"https://${YOUR GITHUB REPO}blob/master/${PATH TO FOLDER}/template.yaml\"}" ``` diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index b9d215fd8a..03494564c3 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -57,8 +57,8 @@ The default storage and request URLs: ```yaml techdocs: - storageUrl: http://localhost:7000/techdocs/static/docs - requestUrl: http://localhost:7000/techdocs/docs + storageUrl: http://localhost:7000/api/techdocs/static/docs + requestUrl: http://localhost:7000/api/techdocs/docs ``` If you want `techdocs-backend` to manage building and publishing, you want diff --git a/docs/features/techdocs/troubleshooting.md b/docs/features/techdocs/troubleshooting.md new file mode 100644 index 0000000000..3a9b2fcfd0 --- /dev/null +++ b/docs/features/techdocs/troubleshooting.md @@ -0,0 +1,10 @@ +--- +id: troubleshooting +title: Troubleshooting TechDocs +sidebar_label: Troubleshooting +description: Troubleshooting for TechDocs +--- + +- TechDocs will fail to clone your docs if you have a git config which overrides + the `https` protocol with `ssh` or something else. Make sure to remove your + git config locally when you try TechDocs. diff --git a/docs/openapi/definitions/auth.yaml b/docs/openapi/definitions/auth.yaml index eb96d434c2..cb736fd2a9 100644 --- a/docs/openapi/definitions/auth.yaml +++ b/docs/openapi/definitions/auth.yaml @@ -6,10 +6,10 @@ info: **Provided by `@backstage/auth-backend`.** - The purpose of the Auth APIs in Backstage are to identify the user, and to provide a way for plugins + The purpose of the Auth APIs in Backstage are to identify the user, and to provide a way for plugins to request access to 3rd party services on behalf of that user. - The API is supplied with a list of providers - such as `Google` or `Github` - and will add the endpoints + The API is supplied with a list of providers - such as `Google` or `Github` - and will add the endpoints described below to each of those providers. Read more about [User Authentication and Authorization in Backstage](https://github.com/spotify/backstage/blob/master/docs/auth/overview.md). @@ -21,7 +21,7 @@ externalDocs: description: Backstage official documentation url: https://github.com/spotify/backstage/blob/master/docs/README.md servers: - - url: http://localhost:7000/auth/ + - url: http://localhost:7000/api/auth/ tags: - name: provider description: List of endpoints per provider diff --git a/docs/overview/architecture-overview.md b/docs/overview/architecture-overview.md index f2c6dad61f..8edf3e9e4c 100644 --- a/docs/overview/architecture-overview.md +++ b/docs/overview/architecture-overview.md @@ -4,6 +4,23 @@ title: Architecture overview description: Documentation on Architecture overview --- +## Terminology + +Backstage is constructed out of three parts. We separate Backstage in this way +because we see three groups of contributors that work with Backstage in three +different ways. + +- Core - Base functionality built by core developers in the open source project. +- App - The app is an instance of a Backstage app that is deployed and tweaked. + The app ties together core functionality with additional plugins. The app is + built and maintained by app developers, usually a productivity team within a + company. +- Plugins - Additional functionality to make your Backstage app useful for your + company. Plugins can be specific to a company or open sourced and reusable. At + Spotify we have over 100 plugins built by over 50 different teams. It has been + very powerful to get contributions from various infrastructure teams added + into a single unified developer experience. + ## Overview The following diagram shows how Backstage might look when deployed inside a diff --git a/docs/overview/architecture-terminology.md b/docs/overview/architecture-terminology.md deleted file mode 100644 index 092ca7fb01..0000000000 --- a/docs/overview/architecture-terminology.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -id: architecture-terminology -title: Architecture terminology -description: Documentation on Architecture terminology ---- - -Backstage is constructed out of three parts. We separate Backstage in this way -because we see three groups of contributors that work with Backstage in three -different ways. - -- Core - Base functionality built by core devs in the open source project. -- App - The app is an instance of a Backstage app that is deployed and tweaked. - The app ties together core functionality with additional plugins. The app is - built and maintained by app developers, usually a productivity team within a - company. -- Plugins - Additional functionality to make your Backstage app useful for your - company. Plugins can be specific to a company or open sourced and reusable. At - Spotify we have over 100 plugins built by over 50 different teams. It has been - very powerful to get contributions from various infrastructure teams added - into a single unified developer experience. diff --git a/docs/overview/background.md b/docs/overview/background.md index 486260bac3..aca46614c2 100644 --- a/docs/overview/background.md +++ b/docs/overview/background.md @@ -1,7 +1,8 @@ --- id: background title: The Spotify Story -description: Documentation on Background and Story behind making of Backstage +description: Backstage was born out of necessity at Spotify. We found that as we grew, our +infrastructure was becoming more fragmented, our engineers less productive. --- Backstage was born out of necessity at Spotify. We found that as we grew, our @@ -29,3 +30,9 @@ building a new microservice using an automated template in Backstage. Create, maintain, and find the documentation for all that software in Backstage. One place for everything. Accessible to everyone. + +Backstage was originally built by Spotify and then donated to the CNCF. +Backstage is currently in the Sandbox phase. Read the announcement +[here](https://backstage.io/blog/2020/09/23/backstage-cncf-sandbox). + + diff --git a/docs/overview/support.md b/docs/overview/support.md index 6e00c17fab..12bc7cfaf9 100644 --- a/docs/overview/support.md +++ b/docs/overview/support.md @@ -13,10 +13,7 @@ description: Support and Community Details and Links - [FAQ](../FAQ.md) - Frequently Asked Questions - [Code of Conduct](../../CODE_OF_CONDUCT.md) - This is how we roll - [Blog](https://backstage.io/blog/) - Announcements and updates -- [Newsletter](https://mailchi.mp/spotify/backstage-community) +- [Newsletter](https://mailchi.mp/spotify/backstage-community) - Subscribe to + our email newsletter - Give us a star ⭐️ - If you are using Backstage or think it is an interesting project, we would love a star ❤️ - -Or, if you are an open source developer and are interested in joining our team, -please reach out to -[foss-opportunities@spotify.com ](mailto:foss-opportunities@spotify.com) diff --git a/docs/overview/what-is-backstage.md b/docs/overview/what-is-backstage.md index 0d16943c9b..3d41c714ee 100644 --- a/docs/overview/what-is-backstage.md +++ b/docs/overview/what-is-backstage.md @@ -1,7 +1,7 @@ --- id: what-is-backstage title: What is Backstage? -description: Backsatge is an open platform for building developer portals. +description: Backstage is an open platform for building developer portals. Powered by a centralized service catalog, Backstage restores order to your microservices and infrastructure --- @@ -33,7 +33,14 @@ Out of the box, Backstage includes: [open source plugins](https://github.com/spotify/backstage/tree/master/plugins) that further expand Backstage’s customizability and functionality -### Benefits +## Backstage and the CNCF + +Backstage is a CNCF Sandbox project. Read the announcement +[here](https://backstage.io/blog/2020/09/23/backstage-cncf-sandbox). + + + +## Benefits - For _engineering managers_, it allows you to maintain standards and best practices across the organization, and can help you manage your whole tech diff --git a/docs/tutorials/quickstart-app-auth.md b/docs/tutorials/quickstart-app-auth.md index 82f9cab537..00494c7768 100644 --- a/docs/tutorials/quickstart-app-auth.md +++ b/docs/tutorials/quickstart-app-auth.md @@ -115,7 +115,7 @@ export AUTH_GITHUB_CLIENT_SECRET=xxx > Log into http://github.com > Navigate to (Settings > Developer Settings > OAuth Apps > New OAuth App)[https://github.com/settings/applications/new] > Set Homepage URL = http://localhost:3000 -> Set Callback URL = http://localhost:7000/auth/github +> Set Callback URL = http://localhost:7000/api/auth/github > Click [Register application] > On the next page, copy and paste your new Client ID and Client Secret to the environment variables above, `AUTH_GITHUB_CLIENT_ID` & `AUTH_GITHUB_CLIENT_SECRET` > Don't forget to `source` that profile file again if necessary. diff --git a/lerna.json b/lerna.json index a018b1a289..d43d4ff160 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,5 @@ "packages": ["packages/*", "plugins/*"], "npmClient": "yarn", "useWorkspaces": true, - "version": "0.1.1-alpha.22" + "version": "0.1.1-alpha.23" } diff --git a/microsite/core/Footer.js b/microsite/core/Footer.js index 761862c1fc..8e3c4c086d 100644 --- a/microsite/core/Footer.js +++ b/microsite/core/Footer.js @@ -51,6 +51,7 @@ class Footer extends React.Component { Subscribe to our newsletter + CNCF Sandbox
More
diff --git a/microsite/sidebars.json b/microsite/sidebars.json index 8fa4935a38..7081e2bb1c 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -3,7 +3,6 @@ "Overview": [ "overview/what-is-backstage", "overview/architecture-overview", - "overview/architecture-terminology", "overview/roadmap", "overview/vision", "overview/background", @@ -43,6 +42,7 @@ "features/software-catalog/configuration", "features/software-catalog/system-model", "features/software-catalog/descriptor-format", + "features/software-catalog/references", "features/software-catalog/well-known-annotations", "features/software-catalog/extending-the-model", "features/software-catalog/external-integrations", @@ -71,6 +71,7 @@ "features/techdocs/concepts", "features/techdocs/architecture", "features/techdocs/creating-and-publishing", + "features/techdocs/troubleshooting", "features/techdocs/faqs" ] } @@ -158,7 +159,8 @@ "architecture-decisions/adrs-adr005", "architecture-decisions/adrs-adr006", "architecture-decisions/adrs-adr007", - "architecture-decisions/adrs-adr008" + "architecture-decisions/adrs-adr008", + "architecture-decisions/adrs-adr009" ], "Contribute": ["../CONTRIBUTING"], "Support": ["overview/support"], diff --git a/mkdocs.yml b/mkdocs.yml index ba607f8a6e..4fbec4650d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,7 +5,6 @@ nav: - Overview: - What is Backstage?: 'overview/what-is-backstage.md' - Backstage architecture: 'overview/architecture-overview.md' - - Architecture and terminology: 'overview/architecture-terminology.md' - Roadmap: 'overview/roadmap.md' - Vision: 'overview/vision.md' - The Spotify story: 'overview/background.md' @@ -32,6 +31,7 @@ nav: - Configuration: 'features/software-catalog/configuration.md' - System model: 'features/software-catalog/system-model.md' - YAML File Format: 'features/software-catalog/descriptor-format.md' + - Entity References: 'features/software-catalog/references.md' - Well-known Annotations: 'features/software-catalog/well-known-annotations.md' - Extending the model: 'features/software-catalog/extending-the-model.md' - External integrations: 'features/software-catalog/external-integrations.md' @@ -51,6 +51,7 @@ nav: - Concepts: 'features/techdocs/concepts.md' - TechDocs Architecture: 'features/techdocs/architecture.md' - Creating and Publishing Documentation: 'features/techdocs/creating-and-publishing.md' + - Troubleshooting: 'features/techdocs/troubleshooting.md' - FAQ: 'features/techdocs/FAQ.md' - Plugins: - Overview: 'plugins/index.md' @@ -104,6 +105,7 @@ nav: - ADR006 - Avoid React.FC and React.SFC: 'architecture-decisions/adr006-avoid-react-fc.md' - ADR007 - Use MSW for Network Request Mocking: 'architecture-decisions/adr007-use-msw-to-mock-service-requests.md' - ADR008 - Default Catalog File Name: 'architecture-decisions/adr008-default-catalog-file-name.md' + - ADR009 - Entity References: 'architecture-decisions/adr009-entity-references.md' - Contribute: '../CONTRIBUTING.md' - Support: 'overview/support.md' - FAQ: FAQ.md diff --git a/packages/app/package.json b/packages/app/package.json index 1262c954cc..77b1882b47 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,33 +1,33 @@ { "name": "example-app", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/plugin-api-docs": "^0.1.1-alpha.22", - "@backstage/plugin-catalog": "^0.1.1-alpha.22", - "@backstage/plugin-circleci": "^0.1.1-alpha.22", - "@backstage/plugin-explore": "^0.1.1-alpha.22", - "@backstage/plugin-gcp-projects": "^0.1.1-alpha.22", - "@backstage/plugin-github-actions": "^0.1.1-alpha.22", - "@backstage/plugin-gitops-profiles": "^0.1.1-alpha.22", - "@backstage/plugin-graphiql": "^0.1.1-alpha.22", - "@backstage/plugin-jenkins": "^0.1.1-alpha.22", - "@backstage/plugin-kubernetes": "^0.1.1-alpha.22", - "@backstage/plugin-lighthouse": "^0.1.1-alpha.22", - "@backstage/plugin-newrelic": "^0.1.1-alpha.22", - "@backstage/plugin-register-component": "^0.1.1-alpha.22", - "@backstage/plugin-rollbar": "^0.1.1-alpha.22", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.22", - "@backstage/plugin-sentry": "^0.1.1-alpha.22", - "@backstage/plugin-tech-radar": "^0.1.1-alpha.22", - "@backstage/plugin-techdocs": "^0.1.1-alpha.22", - "@backstage/plugin-welcome": "^0.1.1-alpha.22", - "@backstage/test-utils": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@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-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", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@octokit/rest": "^18.0.0", diff --git a/packages/app/src/App.test.tsx b/packages/app/src/App.test.tsx index cddd093855..40c49416bf 100644 --- a/packages/app/src/App.test.tsx +++ b/packages/app/src/App.test.tsx @@ -31,7 +31,7 @@ describe('App', () => { baseUrl: 'http://localhost:3003', }, techdocs: { - storageUrl: 'http://localhost:7000/techdocs/static/docs', + storageUrl: 'http://localhost:7000/api/techdocs/static/docs', }, }, context: 'test', diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index 7f57f6f970..581a591152 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -16,11 +16,8 @@ import { errorApiRef, - discoveryApiRef, - UrlPatternDiscovery, githubAuthApiRef, createApiFactory, - configApiRef, } from '@backstage/core'; import { @@ -38,15 +35,6 @@ import { } from '@roadiehq/backstage-plugin-github-pull-requests'; export const apis = [ - // TODO(Rugvip): migrate to use /api - createApiFactory({ - api: discoveryApiRef, - deps: { configApi: configApiRef }, - factory: ({ configApi }) => - UrlPatternDiscovery.compile( - `${configApi.getString('backend.baseUrl')}/{{ pluginId }}`, - ), - }), createApiFactory({ api: graphQlBrowseApiRef, deps: { errorApi: errorApiRef, githubAuthApi: githubAuthApiRef }, diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 9d8a7061e0..72f047761d 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.22", + "version": "0.1.1-alpha.23", "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.22", - "@backstage/config": "^0.1.1-alpha.22", - "@backstage/config-loader": "^0.1.1-alpha.22", + "@backstage/cli-common": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", + "@backstage/config-loader": "^0.1.1-alpha.23", "@types/cors": "^2.8.6", "@types/express": "^4.17.6", "compression": "^1.7.4", @@ -58,7 +58,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", "@types/compression": "^1.7.0", "@types/http-errors": "^1.6.3", "@types/morgan": "^1.9.0", diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 6f7500bb86..247eb593cc 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -23,7 +23,7 @@ import { loadConfig } from '@backstage/config-loader'; export async function loadBackendConfig() { const paths = findPaths(__dirname); const configs = await loadConfig({ - env: process.env.NODE_ENV, + env: process.env.NODE_ENV ?? 'development', rootPaths: [paths.targetRoot, paths.targetDir], shouldReadSecrets: true, }); diff --git a/packages/backend/package.json b/packages/backend/package.json index 147ea630c2..5bfaf2ee46 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "dist/index.cjs.js", "types": "src/index.ts", "private": true, @@ -18,25 +18,26 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.22", - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/config": "^0.1.1-alpha.22", - "@backstage/plugin-app-backend": "^0.1.1-alpha.22", - "@backstage/plugin-auth-backend": "^0.1.1-alpha.22", - "@backstage/plugin-catalog-backend": "^0.1.1-alpha.22", - "@backstage/plugin-graphql-backend": "^0.1.1-alpha.22", - "@backstage/plugin-identity-backend": "^0.1.1-alpha.22", - "@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.22", - "@backstage/plugin-proxy-backend": "^0.1.1-alpha.22", - "@backstage/plugin-rollbar-backend": "^0.1.1-alpha.22", - "@backstage/plugin-scaffolder-backend": "^0.1.1-alpha.22", - "@backstage/plugin-sentry-backend": "^0.1.1-alpha.22", - "@backstage/plugin-techdocs-backend": "^0.1.1-alpha.22", + "@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-identity-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", "@gitbeaker/node": "^23.5.0", "@octokit/rest": "^18.0.0", "dockerode": "^3.2.0", - "example-app": "^0.1.1-alpha.22", + "example-app": "^0.1.1-alpha.23", "express": "^4.17.1", + "express-promise-router": "^3.0.3", "knex": "^0.21.1", "pg": "^8.3.0", "pg-connection-string": "^2.3.0", @@ -44,7 +45,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", "@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 7c52175d76..9d4a738ecf 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -22,12 +22,14 @@ * Happy hacking! */ +import Router from 'express-promise-router'; import { createDatabaseClient, createServiceBuilder, loadBackendConfig, getRootLogger, useHotMemoize, + notFoundHandler, } from '@backstage/backend-common'; import { ConfigReader, AppConfig } from '@backstage/config'; import healthcheck from './plugins/healthcheck'; @@ -78,19 +80,23 @@ async function main() { const graphqlEnv = useHotMemoize(module, () => createEnv('graphql')); const appEnv = useHotMemoize(module, () => createEnv('app')); + const apiRouter = Router(); + apiRouter.use('/catalog', await catalog(catalogEnv)); + apiRouter.use('/rollbar', await rollbar(rollbarEnv)); + apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)); + apiRouter.use('/sentry', await sentry(sentryEnv)); + apiRouter.use('/auth', await auth(authEnv, '/api/auth')); + apiRouter.use('/identity', await identity(identityEnv)); + apiRouter.use('/techdocs', await techdocs(techdocsEnv)); + apiRouter.use('/kubernetes', await kubernetes(kubernetesEnv)); + apiRouter.use('/proxy', await proxy(proxyEnv, '/api/proxy')); + apiRouter.use('/graphql', await graphql(graphqlEnv)); + apiRouter.use(notFoundHandler()); + const service = createServiceBuilder(module) .loadConfig(configReader) .addRouter('', await healthcheck(healthcheckEnv)) - .addRouter('/catalog', await catalog(catalogEnv)) - .addRouter('/rollbar', await rollbar(rollbarEnv)) - .addRouter('/scaffolder', await scaffolder(scaffolderEnv)) - .addRouter('/sentry', await sentry(sentryEnv)) - .addRouter('/auth', await auth(authEnv)) - .addRouter('/identity', await identity(identityEnv)) - .addRouter('/techdocs', await techdocs(techdocsEnv)) - .addRouter('/kubernetes', await kubernetes(kubernetesEnv)) - .addRouter('/proxy', await proxy(proxyEnv, '/proxy')) - .addRouter('/graphql', await graphql(graphqlEnv)) + .addRouter('/api', apiRouter) .addRouter('', await app(appEnv)); await service.start().catch(err => { diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index b24dd6ca6b..60baaff6b7 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -17,10 +17,9 @@ import { createRouter } from '@backstage/plugin-auth-backend'; import { PluginEnvironment } from '../types'; -export default async function createPlugin({ - logger, - database, - config, -}: PluginEnvironment) { - return await createRouter({ logger, config, database }); +export default async function createPlugin( + { logger, database, config }: PluginEnvironment, + basePath: string, +) { + return await createRouter({ logger, config, database, basePath }); } diff --git a/packages/backend/src/plugins/kubernetes.ts b/packages/backend/src/plugins/kubernetes.ts index 42b7722ff6..7906765533 100644 --- a/packages/backend/src/plugins/kubernetes.ts +++ b/packages/backend/src/plugins/kubernetes.ts @@ -17,6 +17,9 @@ import { createRouter } from '@backstage/plugin-kubernetes-backend'; import { PluginEnvironment } from '../types'; -export default async function createPlugin({ logger }: PluginEnvironment) { - return await createRouter({ logger }); +export default async function createPlugin({ + logger, + config, +}: PluginEnvironment) { + return await createRouter({ logger, config }); } diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index d683eec68d..96433a0f0e 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.22", + "version": "0.1.1-alpha.23", "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.22", + "@backstage/config": "^0.1.1-alpha.23", "@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.22", + "@backstage/cli": "^0.1.1-alpha.23", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/catalog-model/src/EntityPolicies.ts b/packages/catalog-model/src/EntityPolicies.ts index 8cc51e5aa8..3e2f51b40a 100644 --- a/packages/catalog-model/src/EntityPolicies.ts +++ b/packages/catalog-model/src/EntityPolicies.ts @@ -28,6 +28,7 @@ import { GroupEntityV1alpha1Policy, LocationEntityV1alpha1Policy, TemplateEntityV1alpha1Policy, + UserEntityV1alpha1Policy, } from './kinds'; import { EntityPolicy } from './types'; @@ -77,6 +78,7 @@ export class EntityPolicies implements EntityPolicy { EntityPolicies.anyOf([ new ComponentEntityV1alpha1Policy(), new GroupEntityV1alpha1Policy(), + new UserEntityV1alpha1Policy(), new LocationEntityV1alpha1Policy(), new TemplateEntityV1alpha1Policy(), new ApiEntityV1alpha1Policy(), diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index 380f5458cc..a6e90e6975 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -17,6 +17,7 @@ export { entityMetaGeneratedFields } from './Entity'; export type { Entity, EntityMeta } from './Entity'; export * from './policies'; +export { parseEntityName, serializeEntityRef } from './ref'; export { entityHasChanges, generateEntityEtag, diff --git a/packages/catalog-model/src/entity/ref.test.ts b/packages/catalog-model/src/entity/ref.test.ts new file mode 100644 index 0000000000..b76f01f1f9 --- /dev/null +++ b/packages/catalog-model/src/entity/ref.test.ts @@ -0,0 +1,362 @@ +/* + * 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 { parseEntityName, parseEntityRef, serializeEntityRef } from './ref'; + +describe('ref', () => { + describe('parseEntityName', () => { + it('handles some omissions', () => { + expect(parseEntityName('a:b/c')).toEqual({ + kind: 'a', + namespace: 'b', + name: 'c', + }); + expect(() => parseEntityName('b/c')).toThrow(/kind/); + expect(parseEntityName('a:c')).toEqual({ + kind: 'a', + namespace: 'default', + name: 'c', + }); + expect(() => parseEntityName('c')).toThrow(/kind/); + }); + + it('rejects bad inputs', () => { + expect(() => parseEntityName(null as any)).toThrow(); + expect(() => parseEntityName(7 as any)).toThrow(); + expect(() => parseEntityName('a:b:c')).toThrow(); + expect(() => parseEntityName('a/b/c')).toThrow(); + expect(() => parseEntityName('a/b:c')).toThrow(); + expect(() => parseEntityName('a:b/c/d')).toThrow(); + expect(() => parseEntityName('a:b/c:d')).toThrow(); + }); + + it('rejects empty parts in strings', () => { + // one is empty + expect(() => parseEntityName(':b/c')).toThrow(); + expect(() => parseEntityName('a:/c')).toThrow(); + expect(() => parseEntityName('a:b/')).toThrow(); + // two are empty + expect(() => parseEntityName('a:/')).toThrow(); + expect(() => parseEntityName(':b/')).toThrow(); + expect(() => parseEntityName(':/c')).toThrow(); + // three are empty + expect(() => parseEntityName(':/')).toThrow(); + // one is left out, one empty + expect(() => parseEntityName('/c')).toThrow(); + expect(() => parseEntityName('b/')).toThrow(); + expect(() => parseEntityName(':c')).toThrow(); + expect(() => parseEntityName('a:')).toThrow(); + // nothing at all + expect(() => parseEntityName('')).toThrow(); + }); + + it('rejects empty parts in compounds', () => { + // one is empty + expect(() => + parseEntityName({ kind: '', namespace: 'b', name: 'c' }), + ).toThrow(); + expect(() => parseEntityName({ namespace: 'b', name: 'c' })).toThrow(); + expect(() => + parseEntityName({ kind: 'a', namespace: '', name: 'c' }), + ).toThrow(); + expect(() => parseEntityName({ kind: 'a', name: 'c' })).not.toThrow(); + expect(() => + parseEntityName({ kind: 'a', namespace: 'b', name: '' }), + ).toThrow(); + expect(() => + parseEntityName({ kind: 'a', namespace: 'b' } as any), + ).toThrow(); + // two are empty + expect(() => + parseEntityName({ kind: '', namespace: '', name: 'c' }), + ).toThrow(); + expect(() => parseEntityName({ name: 'c' })).toThrow(); + expect(() => + parseEntityName({ kind: '', namespace: 'b', name: '' }), + ).toThrow(); + expect(() => parseEntityName({ namespace: 'b' } as any)).toThrow(); + expect(() => + parseEntityName({ kind: 'a', namespace: '', name: '' }), + ).toThrow(); + expect(() => parseEntityName({ kind: 'a' } as any)).toThrow(); + // three are empty + expect(() => + parseEntityName({ kind: '', namespace: '', name: '' }), + ).toThrow(); + expect(() => parseEntityName({} as any)).toThrow(); + // one is left out, one empty + expect(() => parseEntityName({ namespace: '', name: 'c' })).toThrow(); + expect(() => parseEntityName({ namespace: 'b', name: '' })).toThrow(); + expect(() => parseEntityName({ kind: '', name: 'c' })).toThrow(); + expect(() => parseEntityName({ kind: 'a', name: '' })).toThrow(); + }); + + it('adds defaults where necessary to strings', () => { + expect( + parseEntityName('a:b/c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c' }); + expect( + parseEntityName('b/c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'x', namespace: 'b', name: 'c' }); + expect( + parseEntityName('a:c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); + expect(parseEntityName('a:c', { defaultKind: 'x' })).toEqual({ + kind: 'a', + namespace: 'default', + name: 'c', + }); + expect( + parseEntityName('c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); + expect(parseEntityName('c', { defaultKind: 'x' })).toEqual({ + kind: 'x', + namespace: 'default', + name: 'c', + }); + }); + + it('adds defaults where necessary to compounds', () => { + expect( + parseEntityName( + { kind: 'a', namespace: 'b', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c' }); + expect( + parseEntityName( + { namespace: 'b', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'x', namespace: 'b', name: 'c' }); + expect( + parseEntityName( + { kind: 'a', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); + expect( + parseEntityName({ kind: 'a', name: 'c' }, { defaultKind: 'x' }), + ).toEqual({ kind: 'a', namespace: 'default', name: 'c' }); + expect( + parseEntityName( + { name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); + expect(parseEntityName({ name: 'c' }, { defaultKind: 'x' })).toEqual({ + kind: 'x', + namespace: 'default', + name: 'c', + }); + // empty strings are errors, not defaults + expect(() => + parseEntityName( + { kind: '', namespace: 'b', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toThrow(/kind/); + expect(() => + parseEntityName( + { kind: 'a', namespace: '', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toThrow(/namespace/); + }); + }); + + describe('parseEntityRef', () => { + it('handles some omissions', () => { + expect(parseEntityRef('a:b/c')).toEqual({ + kind: 'a', + namespace: 'b', + name: 'c', + }); + expect(parseEntityRef('b/c')).toEqual({ + kind: undefined, + namespace: 'b', + name: 'c', + }); + expect(parseEntityRef('a:c')).toEqual({ + kind: 'a', + namespace: undefined, + name: 'c', + }); + expect(parseEntityRef('c')).toEqual({ + kind: undefined, + namespace: undefined, + name: 'c', + }); + }); + + it('rejects bad inputs', () => { + expect(() => parseEntityRef(null as any)).toThrow(); + expect(() => parseEntityRef(7 as any)).toThrow(); + expect(() => parseEntityRef('a:b:c')).toThrow(); + expect(() => parseEntityRef('a/b/c')).toThrow(); + expect(() => parseEntityRef('a/b:c')).toThrow(); + expect(() => parseEntityRef('a:b/c/d')).toThrow(); + expect(() => parseEntityRef('a:b/c:d')).toThrow(); + }); + + it('rejects empty parts in strings', () => { + // one is empty + expect(() => parseEntityRef(':b/c')).toThrow(); + expect(() => parseEntityRef('a:/c')).toThrow(); + expect(() => parseEntityRef('a:b/')).toThrow(); + // two are empty + expect(() => parseEntityRef('a:/')).toThrow(); + expect(() => parseEntityRef(':b/')).toThrow(); + expect(() => parseEntityRef(':/c')).toThrow(); + // three are empty + expect(() => parseEntityRef(':/')).toThrow(); + // one is left out, one empty + expect(() => parseEntityRef('/c')).toThrow(); + expect(() => parseEntityRef('b/')).toThrow(); + expect(() => parseEntityRef(':c')).toThrow(); + expect(() => parseEntityRef('a:')).toThrow(); + // nothing at all + expect(() => parseEntityRef('')).toThrow(); + }); + + it('rejects empty parts in compounds', () => { + // one is empty + expect(() => + parseEntityRef({ kind: '', namespace: 'b', name: 'c' }), + ).toThrow(); + expect(() => + parseEntityRef({ kind: 'a', namespace: '', name: 'c' }), + ).toThrow(); + expect(() => + parseEntityRef({ kind: 'a', namespace: 'b', name: '' }), + ).toThrow(); + // two are empty + expect(() => + parseEntityRef({ kind: '', namespace: '', name: 'c' }), + ).toThrow(); + expect(() => + parseEntityRef({ kind: '', namespace: 'b', name: '' }), + ).toThrow(); + expect(() => + parseEntityRef({ kind: 'a', namespace: '', name: '' }), + ).toThrow(); + // three are empty + expect(() => + parseEntityRef({ kind: '', namespace: '', name: '' }), + ).toThrow(); + // one is left out, one empty + expect(() => parseEntityRef({ namespace: '', name: 'c' })).toThrow(); + expect(() => parseEntityRef({ namespace: 'b', name: '' })).toThrow(); + expect(() => parseEntityRef({ kind: '', name: 'c' })).toThrow(); + expect(() => parseEntityRef({ kind: 'a', name: '' })).toThrow(); + // nothing at all + expect(() => parseEntityRef({} as any)).toThrow(); + }); + + it('adds defaults where necessary to strings', () => { + expect( + parseEntityRef('a:b/c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c' }); + expect( + parseEntityRef('b/c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'x', namespace: 'b', name: 'c' }); + expect( + parseEntityRef('a:c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); + expect( + parseEntityRef('c', { defaultKind: 'x', defaultNamespace: 'y' }), + ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); + }); + + it('adds defaults where necessary to compounds', () => { + expect( + parseEntityRef( + { kind: 'a', namespace: 'b', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c' }); + expect( + parseEntityRef( + { namespace: 'b', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'x', namespace: 'b', name: 'c' }); + expect( + parseEntityRef( + { kind: 'a', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'a', namespace: 'y', name: 'c' }); + expect( + parseEntityRef( + { name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toEqual({ kind: 'x', namespace: 'y', name: 'c' }); + // empty strings are errors, not defaults + expect(() => + parseEntityRef( + { kind: '', namespace: 'b', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toThrow(/kind/); + expect(() => + parseEntityRef( + { kind: 'a', namespace: '', name: 'c' }, + { defaultKind: 'x', defaultNamespace: 'y' }, + ), + ).toThrow(/namespace/); + }); + }); + + describe('serializeEntityRef', () => { + it('handles partials', () => { + expect( + serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c' }), + ).toEqual('a:b/c'); + expect(serializeEntityRef({ namespace: 'b', name: 'c' })).toEqual('b/c'); + expect(serializeEntityRef({ kind: 'a', name: 'c' })).toEqual('a:c'); + expect(serializeEntityRef({ name: 'c' })).toEqual('c'); + }); + + it('picks the least complex form', () => { + expect( + serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c' }), + ).toEqual('a:b/c'); + expect(serializeEntityRef({ namespace: 'b', name: 'c' })).toEqual('b/c'); + expect(serializeEntityRef({ kind: 'a', name: 'c' })).toEqual('a:c'); + expect(serializeEntityRef({ name: 'c' })).toEqual('c'); + expect( + serializeEntityRef({ kind: 'a:x', namespace: 'b', name: 'c' }), + ).toEqual({ kind: 'a:x', namespace: 'b', name: 'c' }); + expect( + serializeEntityRef({ kind: 'a/x', namespace: 'b', name: 'c' }), + ).toEqual({ kind: 'a/x', namespace: 'b', name: 'c' }); + expect( + serializeEntityRef({ kind: 'a', namespace: 'b:x', name: 'c' }), + ).toEqual({ kind: 'a', namespace: 'b:x', name: 'c' }); + expect( + serializeEntityRef({ kind: 'a', namespace: 'b/x', name: 'c' }), + ).toEqual({ kind: 'a', namespace: 'b/x', name: 'c' }); + expect( + serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c:x' }), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c:x' }); + expect( + serializeEntityRef({ kind: 'a', namespace: 'b', name: 'c/x' }), + ).toEqual({ kind: 'a', namespace: 'b', name: 'c/x' }); + }); + }); +}); diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts new file mode 100644 index 0000000000..b9320a5ac7 --- /dev/null +++ b/packages/catalog-model/src/entity/ref.ts @@ -0,0 +1,164 @@ +/* + * 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 { EntityName, EntityRef } from '../types'; + +/** + * The context of defaults that entity reference parsing happens within. + */ +type EntityRefContext = { + /** The default kind, if none is given in the reference */ + defaultKind?: string; + /** The default namespace, if none is given in the reference */ + defaultNamespace?: string; +}; + +/** + * Parses an entity reference, either on string or compound form, and always + * returns a complete entity name including kind, namespace and name. + * + * This function automatically assumes the default namespace "default" unless + * otherwise specified as part of the options, and will throw an error if no + * kind was specified in the input reference and no default kind was given. + * + * @param ref The reference to parse + * @param context The context of defaults that the parsing happens within + * @returns A complete entity name + */ +export function parseEntityName( + ref: EntityRef, + context: EntityRefContext = {}, +): EntityName { + const { kind, namespace, name } = parseEntityRef(ref, { + defaultNamespace: 'default', + ...context, + }); + + if (!kind) { + throw new Error( + `Entity reference ${namespace}/${name} did not contain a kind`, + ); + } + + return { kind, namespace, name }; +} + +/** + * Parses an entity reference, either on string or compound form, and returns + * a structure with a name, and optional kind and namespace. + * + * The options object can contain default values for the kind and namespace, + * that will be used if the input reference did not specify any. + * + * @param ref The reference to parse + * @param context The context of defaults that the parsing happens within + * @returns The compound form of the reference + */ +export function parseEntityRef( + ref: EntityRef, + context?: { defaultKind: string }, +): { + kind: string; + namespace?: string; + name: string; +}; +export function parseEntityRef( + ref: EntityRef, + context?: { defaultNamespace: string }, +): { + kind?: string; + namespace: string; + name: string; +}; +export function parseEntityRef( + ref: EntityRef, + context?: { defaultKind: string; defaultNamespace: string }, +): { + kind: string; + namespace: string; + name: string; +}; +export function parseEntityRef( + ref: EntityRef, + context: EntityRefContext = {}, +): { + kind?: string; + namespace?: string; + name: string; +} { + if (!ref) { + throw new Error(`Entity reference must not be empty`); + } + + if (typeof ref === 'string') { + const match = /^([^:/]+:)?([^:/]+\/)?([^:/]+)$/.exec(ref.trim()); + if (!match) { + throw new Error( + `Entity reference "${ref}" was not on the form [:][/]`, + ); + } + + return { + kind: match[1]?.slice(0, -1) ?? context.defaultKind, + namespace: match[2]?.slice(0, -1) ?? context.defaultNamespace, + name: match[3], + }; + } + + const { kind, namespace, name } = ref; + if (kind === '') { + throw new Error('Entity reference kinds must not be empty'); + } else if (namespace === '') { + throw new Error('Entity reference namespaces must not be empty'); + } else if (!name) { + throw new Error('Entity references must contain a name'); + } + + return { + kind: kind ?? context.defaultKind, + namespace: namespace ?? context.defaultNamespace, + name, + }; +} + +/** + * Takes an entity reference or name, and outputs an entity reference on the + * most compact form possible. I.e. if the parts do not contain any + * special/reserved characters, it outputs the string form, otherwise it + * outputs the compound form. + * + * @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; + if ( + kind?.includes(':') || + kind?.includes('/') || + namespace?.includes(':') || + namespace?.includes('/') || + name.includes(':') || + name.includes('/') + ) { + return { kind, namespace, name }; + } + + return `${kind ? `${kind}:` : ''}${namespace ? `${namespace}/` : ''}${name}`; +} diff --git a/packages/catalog-model/src/index.ts b/packages/catalog-model/src/index.ts index 9469319b28..f93a4001a5 100644 --- a/packages/catalog-model/src/index.ts +++ b/packages/catalog-model/src/index.ts @@ -18,5 +18,5 @@ export * from './entity'; export { EntityPolicies } from './EntityPolicies'; export * from './kinds'; export * from './location'; -export type { EntityPolicy, JSONSchema } from './types'; +export type { EntityName, EntityPolicy, EntityRef, JSONSchema } from './types'; export * from './validation'; diff --git a/packages/catalog-model/src/kinds/UserEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/UserEntityV1alpha1.test.ts new file mode 100644 index 0000000000..b641c6cad3 --- /dev/null +++ b/packages/catalog-model/src/kinds/UserEntityV1alpha1.test.ts @@ -0,0 +1,150 @@ +/* + * 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 { EntityPolicy } from '../types'; +import { + UserEntityV1alpha1, + UserEntityV1alpha1Policy, +} from './UserEntityV1alpha1'; + +describe('UserV1alpha1Policy', () => { + let entity: UserEntityV1alpha1; + let policy: EntityPolicy; + + beforeEach(() => { + entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'doe', + }, + spec: { + profile: { + displayName: 'John Doe', + email: 'john@doe.org', + picture: 'https://doe.org/john.jpeg', + }, + memberOf: ['team-a', 'developers'], + }, + }; + policy = new UserEntityV1alpha1Policy(); + }); + + it('happy path: accepts valid data', async () => { + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + // root + + it('silently accepts v1beta1 as well', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta1'; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('rejects unknown apiVersion', async () => { + (entity as any).apiVersion = 'backstage.io/v1beta0'; + await expect(policy.enforce(entity)).rejects.toThrow(/apiVersion/); + }); + + it('rejects unknown kind', async () => { + (entity as any).kind = 'Wizard'; + await expect(policy.enforce(entity)).rejects.toThrow(/kind/); + }); + + it('spec accepts unknown additional fields', async () => { + (entity as any).spec.foo = 'data'; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + // profile + + it('accepts missing profile', async () => { + delete (entity as any).spec.profile; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('rejects wrong profile', async () => { + (entity as any).spec.profile = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/profile/); + }); + + it('profile accepts missing displayName', async () => { + delete (entity as any).spec.profile.displayName; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('profile rejects wrong displayName', async () => { + (entity as any).spec.profile.displayName = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/displayName/); + }); + + it('profile rejects empty displayName', async () => { + (entity as any).spec.profile.displayName = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/displayName/); + }); + + it('profile accepts missing email', async () => { + delete (entity as any).spec.profile.email; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('profile rejects wrong email', async () => { + (entity as any).spec.profile.email = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/email/); + }); + + it('profile rejects empty email', async () => { + (entity as any).spec.profile.email = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/email/); + }); + + it('profile accepts missing picture', async () => { + delete (entity as any).spec.profile.picture; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + it('profile rejects wrong picture', async () => { + (entity as any).spec.profile.picture = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/picture/); + }); + + it('profile rejects empty picture', async () => { + (entity as any).spec.profile.picture = ''; + await expect(policy.enforce(entity)).rejects.toThrow(/picture/); + }); + + it('profile accepts unknown additional fields', async () => { + (entity as any).spec.profile.foo = 'data'; + await expect(policy.enforce(entity)).resolves.toBe(entity); + }); + + // memberOf + + it('rejects missing memberOf', async () => { + delete (entity as any).spec.memberOf; + await expect(policy.enforce(entity)).rejects.toThrow(/memberOf/); + }); + + it('rejects wrong memberOf', async () => { + (entity as any).spec.memberOf = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/memberOf/); + }); + + it('rejects wrong memberOf item', async () => { + (entity as any).spec.memberOf[0] = 7; + await expect(policy.enforce(entity)).rejects.toThrow(/memberOf/); + }); +}); diff --git a/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts new file mode 100644 index 0000000000..a6a304f509 --- /dev/null +++ b/packages/catalog-model/src/kinds/UserEntityV1alpha1.ts @@ -0,0 +1,62 @@ +/* + * 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 * as yup from 'yup'; +import type { Entity } from '../entity/Entity'; +import type { EntityPolicy } from '../types'; + +const API_VERSION = ['backstage.io/v1alpha1', 'backstage.io/v1beta1'] as const; +const KIND = 'User' as const; + +export interface UserEntityV1alpha1 extends Entity { + apiVersion: typeof API_VERSION[number]; + kind: typeof KIND; + spec: { + profile?: { + displayName?: string; + email?: string; + picture?: string; + }; + memberOf: string[]; + }; +} + +export class UserEntityV1alpha1Policy implements EntityPolicy { + private schema: yup.Schema; + + constructor() { + this.schema = yup.object>({ + apiVersion: yup.string().required().oneOf(API_VERSION), + kind: yup.string().required().equals([KIND]), + spec: yup + .object({ + profile: yup + .object({ + displayName: yup.string().min(1).notRequired(), + email: yup.string().min(1).notRequired(), + picture: yup.string().min(1).notRequired(), + }) + .notRequired(), + memberOf: yup.array(yup.string()).required(), + }) + .required(), + }); + } + + async enforce(envelope: Entity): Promise { + return await this.schema.validate(envelope, { strict: true }); + } +} diff --git a/packages/catalog-model/src/kinds/index.ts b/packages/catalog-model/src/kinds/index.ts index 6e8d27d204..1ac93b426d 100644 --- a/packages/catalog-model/src/kinds/index.ts +++ b/packages/catalog-model/src/kinds/index.ts @@ -14,6 +14,11 @@ * limitations under the License. */ +export { ApiEntityV1alpha1Policy } from './ApiEntityV1alpha1'; +export type { + ApiEntityV1alpha1 as ApiEntity, + ApiEntityV1alpha1, +} from './ApiEntityV1alpha1'; export { ComponentEntityV1alpha1Policy } from './ComponentEntityV1alpha1'; export type { ComponentEntityV1alpha1 as ComponentEntity, @@ -34,8 +39,8 @@ export type { TemplateEntityV1alpha1 as TemplateEntity, TemplateEntityV1alpha1, } from './TemplateEntityV1alpha1'; -export { ApiEntityV1alpha1Policy } from './ApiEntityV1alpha1'; +export { UserEntityV1alpha1Policy } from './UserEntityV1alpha1'; export type { - ApiEntityV1alpha1 as ApiEntity, - ApiEntityV1alpha1, -} from './ApiEntityV1alpha1'; + UserEntityV1alpha1 as UserEntity, + UserEntityV1alpha1, +} from './UserEntityV1alpha1'; diff --git a/packages/catalog-model/src/types.ts b/packages/catalog-model/src/types.ts index cba6438ccb..3aafdc241a 100644 --- a/packages/catalog-model/src/types.ts +++ b/packages/catalog-model/src/types.ts @@ -34,3 +34,29 @@ export type EntityPolicy = { }; export type JSONSchema = JSONSchema7 & { [key in string]?: JsonValue }; + +/** + * A complete entity name, with the full kind-namespace-name triplet. + */ +export type EntityName = { + kind: string; + namespace: string; + name: string; +}; + +/** + * A reference by name to an entity, either as a compact string representation, + * or as a compound reference structure. + * + * The string representation is on the form [:][/]. + * + * Left-out parts of the reference need to be handled by the application, + * either by rejecting the reference or by falling back to default values. + */ +export type EntityRef = + | string + | { + kind?: string; + namespace?: string; + name: string; + }; diff --git a/packages/cli-common/package.json b/packages/cli-common/package.json index 1340b46ae3..bbd9a67387 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.22", + "version": "0.1.1-alpha.23", "private": false, "main": "src/index.ts", "types": "src/index.ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index e1f51be5b7..6f65658539 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public" @@ -28,9 +28,9 @@ "backstage-cli": "bin/backstage-cli" }, "dependencies": { - "@backstage/cli-common": "^0.1.1-alpha.22", - "@backstage/config": "^0.1.1-alpha.22", - "@backstage/config-loader": "^0.1.1-alpha.22", + "@backstage/cli-common": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", + "@backstage/config-loader": "^0.1.1-alpha.23", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^3.18.5", "@lerna/project": "^3.18.0", diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 247812b6fd..cbaf956bdb 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -231,7 +231,11 @@ export default async (cmd: Command) => { const privatePackage = cmd.private === false ? false : true; const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json')); const appPackage = paths.resolveTargetRoot('packages/app'); - const templateDir = paths.resolveOwn('templates/default-plugin'); + const templateDir = paths.resolveOwn( + cmd.backend + ? 'templates/default-backend-plugin' + : 'templates/default-plugin', + ); const tempDir = resolvePath(os.tmpdir(), answers.id); const pluginDir = isMonoRepo ? paths.resolveTargetRoot('plugins', answers.id) @@ -252,6 +256,7 @@ export default async (cmd: Command) => { await createTemporaryPluginFolder(tempDir); Task.section('Preparing files'); + await templatingTask(templateDir, tempDir, { ...answers, version, @@ -267,7 +272,7 @@ export default async (cmd: Command) => { Task.section('Building the plugin'); await buildPlugin(pluginDir); - if (await fs.pathExists(appPackage)) { + if ((await fs.pathExists(appPackage)) && !cmd.backend) { Task.section('Adding plugin as dependency in app'); await addPluginDependencyToApp(paths.targetRoot, name, version); diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index f1828cef84..fea250cd55 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -61,6 +61,10 @@ export function registerCommands(program: CommanderStatic) { program .command('create-plugin') + .option( + '--backend', + 'Create plugin with the backend dependencies as default', + ) .description('Creates a new plugin in the current repository') .option('--scope ', 'NPM scope') .option('--npm-registry ', 'NPM registry URL') diff --git a/packages/cli/templates/default-backend-plugin/.eslintrc.js b/packages/cli/templates/default-backend-plugin/.eslintrc.js new file mode 100644 index 0000000000..16a033dbc6 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint.backend')], +}; diff --git a/packages/cli/templates/default-backend-plugin/README.md.hbs b/packages/cli/templates/default-backend-plugin/README.md.hbs new file mode 100644 index 0000000000..5c34b40360 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/README.md.hbs @@ -0,0 +1,14 @@ +# {{id}} + +Welcome to the {{id}} backend plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn +start` in the root directory, and then navigating to [/{{id}}](http://localhost:3000/{{id}}). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. diff --git a/packages/cli/templates/default-backend-plugin/package.json.hbs b/packages/cli/templates/default-backend-plugin/package.json.hbs new file mode 100644 index 0000000000..40e612609a --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/package.json.hbs @@ -0,0 +1,44 @@ +{ + "name": "{{name}}", + "version": "{{version}}", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + {{#if privatePackage}} "private": {{privatePackage}}, + {{/if}} + "publishConfig": { + {{#if npmRegistry}} "registry": "{{npmRegistry}}", + {{/if}} + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "start": "backstage-cli backend:dev", + "build": "backstage-cli backend:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/backend-common": "^{{version}}", + "@backstage/config": "^{{version}}", + "@types/express": "^4.17.6", + "express": "^4.17.1", + "express-promise-router": "^3.0.3", + "winston": "^3.2.1", + "node-fetch": "^2.6.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "^{{version}}", + "@types/supertest": "^2.0.8", + "supertest": "^4.0.2", + "msw": "^0.20.5" + }, + "files": [ + "dist" + ] + } diff --git a/packages/cli/templates/default-backend-plugin/src/index.ts b/packages/cli/templates/default-backend-plugin/src/index.ts new file mode 100644 index 0000000000..7612c392a2 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export * from './service/router'; diff --git a/packages/cli/templates/default-backend-plugin/src/run.ts.hbs b/packages/cli/templates/default-backend-plugin/src/run.ts.hbs new file mode 100644 index 0000000000..b96989e4b8 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/run.ts.hbs @@ -0,0 +1,33 @@ +/* + * 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 { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7000; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/packages/cli/templates/default-backend-plugin/src/service/router.test.ts b/packages/cli/templates/default-backend-plugin/src/service/router.test.ts new file mode 100644 index 0000000000..0aaeafa379 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/service/router.test.ts @@ -0,0 +1,45 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; + +import { createRouter } from './router'; + +describe('createRouter', () => { + let app: express.Express; + + beforeAll(async () => { + const router = await createRouter({ + logger: getVoidLogger(), + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /health', () => { + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); +}); diff --git a/packages/cli/templates/default-backend-plugin/src/service/router.ts b/packages/cli/templates/default-backend-plugin/src/service/router.ts new file mode 100644 index 0000000000..3ea8219365 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/service/router.ts @@ -0,0 +1,40 @@ +/* + * 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 { errorHandler } from '@backstage/backend-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; + +export interface RouterOptions { + logger: Logger; +} + +export async function createRouter( + options: RouterOptions, +): Promise { + const { logger } = options; + + const router = Router(); + router.use(express.json()); + + router.get('/health', (_, response) => { + logger.info('PONG!'); + response.send({ status: 'ok' }); + }); + router.use(errorHandler()); + return router; +} diff --git a/packages/cli/templates/default-backend-plugin/src/service/standaloneServer.ts.hbs b/packages/cli/templates/default-backend-plugin/src/service/standaloneServer.ts.hbs new file mode 100644 index 0000000000..6e38965246 --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/service/standaloneServer.ts.hbs @@ -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 { createServiceBuilder } from '@backstage/backend-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: '{{id}}-backend' }); + logger.debug('Starting application server...'); + const router = await createRouter({ + logger, + }); + + const service = createServiceBuilder(module) + .enableCors({ origin: 'http://localhost:3000' }) + .addRouter('/{{id}}', router); + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/packages/cli/templates/default-backend-plugin/src/setupTests.ts b/packages/cli/templates/default-backend-plugin/src/setupTests.ts new file mode 100644 index 0000000000..a5907fd52f --- /dev/null +++ b/packages/cli/templates/default-backend-plugin/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * 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. + */ + +export {}; +global.fetch = require('node-fetch'); diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index 52cffeb621..e024884547 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -41,7 +41,8 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs index 37759313ee..1069790218 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleComponent/ExampleComponent.test.tsx.hbs @@ -1,18 +1,35 @@ import React from 'react'; import { render } from '@testing-library/react'; -import mockFetch from 'jest-fetch-mock'; import ExampleComponent from './ExampleComponent'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + describe('ExampleComponent', () => { + const server = setupServer(); + // Enable API mocking before tests. + beforeAll(() => server.listen()) + + // Reset any runtime request handlers we may add during the tests. + afterEach(() => server.resetHandlers()) + + // Disable API mocking after the tests are done. + afterAll(() => server.close()) + + // setup mock response + beforeEach(() => { + server.use(rest.get('/*', (_, res, ctx) => res(ctx.status(200), ctx.json({})))) + }) + it('should render', () => { - mockFetch.mockResponse(() => new Promise(() => {})); const rendered = render( , - ); - expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument(); + ); + expect(rendered.getByText('Welcome to {{ id }}!')).toBeInTheDocument(); }); }); + \ No newline at end of file diff --git a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs index cc61c215cd..c584289077 100644 --- a/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs +++ b/packages/cli/templates/default-plugin/src/components/ExampleFetchComponent/ExampleFetchComponent.test.tsx.hbs @@ -1,11 +1,25 @@ import React from 'react'; import { render } from '@testing-library/react'; -import mockFetch from 'jest-fetch-mock'; import ExampleFetchComponent from './ExampleFetchComponent'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; describe('ExampleFetchComponent', () => { + const server = setupServer(); + // Enable API mocking before tests. + beforeAll(() => server.listen()) + + // Reset any runtime request handlers we may add during the tests. + afterEach(() => server.resetHandlers()) + + // Disable API mocking after the tests are done. + afterAll(() => server.close()) + + // setup mock response + beforeEach(() => { + server.use(rest.get('https://randomuser.me/*', (_, res, ctx) => res(ctx.status(200), ctx.delay(2000), ctx.json({})))) + }) it('should render', async () => { - mockFetch.mockResponse(() => new Promise(() => {})); const rendered = render(); expect(await rendered.findByTestId('progress')).toBeInTheDocument(); }); diff --git a/packages/cli/templates/default-plugin/src/setupTests.ts b/packages/cli/templates/default-plugin/src/setupTests.ts index 3fa8703ac4..cc559f672e 100644 --- a/packages/cli/templates/default-plugin/src/setupTests.ts +++ b/packages/cli/templates/default-plugin/src/setupTests.ts @@ -1,3 +1,2 @@ import '@testing-library/jest-dom'; - -require('jest-fetch-mock').enableMocks(); +global.fetch = require('node-fetch'); diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 5960b553f7..09f8da3b70 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.22", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.22", + "@backstage/config": "^0.1.1-alpha.23", "fs-extra": "^9.0.0", "yaml": "^1.9.2", "yup": "^0.29.1" diff --git a/packages/config/package.json b/packages/config/package.json index 0b61e50ac6..58c86d6e96 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.22", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 72d0748183..7d338c944d 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.22", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/config": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@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.22", - "@backstage/test-utils-core": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/test-utils-core": "^0.1.1-alpha.23", "@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/package.json b/packages/core/package.json index 440f9f05b0..8749a37b92 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.22", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.22", - "@backstage/core-api": "0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/config": "^0.1.1-alpha.23", + "@backstage/core-api": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -54,8 +54,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/test-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/test-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/packages/create-app/package.json b/packages/create-app/package.json index b303c2423b..46ccfa0b40 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public" @@ -27,7 +27,7 @@ "start": "nodemon --" }, "dependencies": { - "@backstage/cli-common": "^0.1.1-alpha.22", + "@backstage/cli-common": "^0.1.1-alpha.23", "chalk": "^4.0.0", "commander": "^6.1.0", "fs-extra": "^9.0.0", diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index a7d7aa8010..92c517bf93 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -44,8 +44,8 @@ proxy: changeOrigin: true techdocs: - storageUrl: http://localhost:7000/techdocs/static/docs - requestUrl: http://localhost:7000/techdocs/docs + storageUrl: http://localhost:7000/api/techdocs/static/docs + requestUrl: http://localhost:7000/api/techdocs/docs generators: techdocs: 'docker' diff --git a/packages/create-app/templates/default-app/packages/app/src/App.test.tsx b/packages/create-app/templates/default-app/packages/app/src/App.test.tsx index e88c3b47dd..21a2eaa703 100644 --- a/packages/create-app/templates/default-app/packages/app/src/App.test.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/App.test.tsx @@ -12,7 +12,7 @@ describe('App', () => { app: { title: 'Test' }, backend: { baseUrl: 'http://localhost:7000' }, techdocs: { - storageUrl: 'http://localhost:7000/techdocs/static/docs', + storageUrl: 'http://localhost:7000/api/techdocs/static/docs', }, }, context: 'test', diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index adde81e830..b70f36650e 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -31,6 +31,7 @@ "@gitbeaker/node": "^23.5.0", "dockerode": "^3.2.0", "express": "^4.17.1", + "express-promise-router": "^3.0.3", "knex": "^0.21.1", {{#if dbTypePG}} "pg": "^8.3.0", diff --git a/packages/create-app/templates/default-app/packages/backend/src/index.ts b/packages/create-app/templates/default-app/packages/backend/src/index.ts index 6a014727e9..457b95a7a9 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/index.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/index.ts @@ -6,12 +6,14 @@ * Happy hacking! */ +import Router from 'express-promise-router'; import { createDatabaseClient, createServiceBuilder, loadBackendConfig, getRootLogger, useHotMemoize, + notFoundHandler, } from '@backstage/backend-common'; import { ConfigReader, AppConfig } from '@backstage/config'; import auth from './plugins/auth'; @@ -51,14 +53,18 @@ async function main() { const proxyEnv = useHotMemoize(module, () => createEnv('proxy')); const techdocsEnv = useHotMemoize(module, () => createEnv('techdocs')); + const apiRouter = Router(); + apiRouter.use('/catalog', await catalog(catalogEnv)) + apiRouter.use('/scaffolder', await scaffolder(scaffolderEnv)) + apiRouter.use('/auth', await auth(authEnv)) + apiRouter.use('/identity', await identity(identityEnv)) + apiRouter.use('/techdocs', await techdocs(techdocsEnv)) + apiRouter.use('/proxy', await proxy(proxyEnv, '/api/proxy')) + apiRouter.use(notFoundHandler()); + const service = createServiceBuilder(module) .loadConfig(configReader) - .addRouter('/catalog', await catalog(catalogEnv)) - .addRouter('/scaffolder', await scaffolder(scaffolderEnv)) - .addRouter('/auth', await auth(authEnv)) - .addRouter('/identity', await identity(identityEnv)) - .addRouter('/techdocs', await techdocs(techdocsEnv)) - .addRouter('/proxy', await proxy(proxyEnv, '/proxy')); + .addRouter('/api', apiRouter) await service.start().catch(err => { console.log(err); diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 0b6e04f322..54880cee21 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/test-utils": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/test-utils": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/docgen/package.json b/packages/docgen/package.json index 3836af6542..a60a4d1d70 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -1,7 +1,7 @@ { "name": "docgen", "description": "Tool for generating API Documentation for itself", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "private": true, "homepage": "https://backstage.io", "repository": { diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index e518d4f710..fdd8d089c7 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -1,7 +1,7 @@ { "name": "e2e-test", "description": "E2E test for verifying Backstage packages", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "private": true, "homepage": "https://backstage.io", "repository": { @@ -21,7 +21,7 @@ "test:e2e": "yarn start" }, "devDependencies": { - "@backstage/cli-common": "^0.1.1-alpha.22", + "@backstage/cli-common": "^0.1.1-alpha.23", "@types/fs-extra": "^9.0.1", "@types/node": "^13.7.2", "fs-extra": "^9.0.0", diff --git a/packages/e2e-test/src/e2e-test.ts b/packages/e2e-test/src/e2e-test.ts index 969a77cb2a..c7f6137ff1 100644 --- a/packages/e2e-test/src/e2e-test.ts +++ b/packages/e2e-test/src/e2e-test.ts @@ -55,6 +55,9 @@ async function main() { print('Creating a Backstage Plugin'); const pluginName = await createPlugin('test-plugin', appDir); + print('Creating a Backstage Backend Plugin'); + await createPlugin('test-backend-plugin', appDir, ['--backend']); + print('Starting the app'); await testAppServe(pluginName, appDir); @@ -238,8 +241,12 @@ async function overrideModuleResolutions(appDir: string, workspaceDir: string) { /** * Uses create-plugin command to create a new plugin in the app */ -async function createPlugin(pluginName: string, appDir: string) { - const child = spawnPiped(['yarn', 'create-plugin'], { +async function createPlugin( + pluginName: string, + appDir: string, + options: string[] = [], +) { + const child = spawnPiped(['yarn', 'create-plugin', ...options], { cwd: appDir, }); @@ -384,7 +391,7 @@ async function testBackendStart(appDir: string, isPostgres: boolean) { print('Try to fetch entities from the backend'); // Try fetch entities, should be ok - await fetch('http://localhost:7000/catalog/entities').then(res => + await fetch('http://localhost:7000/api/catalog/entities').then(res => res.json(), ); print('Entities fetched successfully'); diff --git a/packages/storybook/package.json b/packages/storybook/package.json index a1decf279a..87d41d47d1 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -1,6 +1,6 @@ { "name": "storybook", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "description": "Storybook build for core package", "private": true, "scripts": { @@ -14,7 +14,7 @@ ] }, "dependencies": { - "@backstage/theme": "^0.1.1-alpha.22" + "@backstage/theme": "^0.1.1-alpha.23" }, "devDependencies": { "@storybook/addon-actions": "^6.0.21", diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 340d9e2aff..6b31b4aae7 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "CLI for running TechDocs locally.", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public" @@ -44,7 +44,7 @@ "ext": "ts" }, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", "commander": "^6.1.0", "fs-extra": "^9.0.1", "http-proxy": "^1.18.1", diff --git a/packages/techdocs-container/techdocs-core/README.md b/packages/techdocs-container/techdocs-core/README.md index 49b9fce4e4..7f34722e81 100644 --- a/packages/techdocs-container/techdocs-core/README.md +++ b/packages/techdocs-container/techdocs-core/README.md @@ -82,6 +82,10 @@ Extensions: ## Changelog +### 0.0.9 + +- Change development status to 3 - Alpha + ### 0.0.8 - Superfences and Codehilite doesn't work very well together (squidfunk/mkdocs-material#1604) so therefore the codehilite extension is replaced by pymdownx.highlight diff --git a/packages/techdocs-container/techdocs-core/setup.py b/packages/techdocs-container/techdocs-core/setup.py index e5a8206150..4385bc2e38 100644 --- a/packages/techdocs-container/techdocs-core/setup.py +++ b/packages/techdocs-container/techdocs-core/setup.py @@ -14,13 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. """ from setuptools import setup, find_packages +from os import path +# read the contents of the README file in the current directory +this_dir = path.abspath(path.dirname(__file__)) +with open(path.join(this_dir, "README.md"), encoding="utf-8") as file: + long_description = file.read() setup( name="mkdocs-techdocs-core", - version="0.0.8", + version="0.0.9", description="A Mkdocs package that contains TechDocs defaults", - long_description="", + long_description=long_description, + long_description_content_type="text/markdown", keywords="mkdocs", url="https://github.com/spotify/backstage", author="TechDocs Core", @@ -37,7 +43,7 @@ setup( "pymdown-extensions==7.1", ], classifiers=[ - "Development Status :: 1 - Planning", + "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Intended Audience :: Information Technology", "License :: OSI Approved :: Apache Software License", diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index d79acf80a8..e30a1e9409 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils-core", "description": "Utilities to test Backstage core", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index cb115fe633..2ca946e028 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/test-utils", "description": "Utilities to test Backstage plugins and apps.", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/core-api": "^0.1.1-alpha.22", - "@backstage/test-utils-core": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/core-api": "^0.1.1-alpha.23", + "@backstage/test-utils-core": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/packages/theme/package.json b/packages/theme/package.json index a1a3d6d111..bd8705933f 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/theme", "description": "material-ui theme for use with Backstage.", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "@material-ui/core": "^4.11.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22" + "@backstage/cli": "^0.1.1-alpha.23" }, "files": [ "dist" diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index 76c0660efb..5ac8e76101 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -82,12 +82,12 @@ export const darkTheme = createTheme({ default: '#333333', }, status: { - ok: '#1DB954', - warning: '#FF9800', - error: '#E22134', - running: '#2E77D0', - pending: '#FFED51', - aborted: '#757575', + ok: '#71CF88', + warning: '#FFB84D', + error: '#F84C55', + running: '#3488E3', + pending: '#FEF071', + aborted: '#9E9E9E', }, bursts: { fontColor: '#FEFEFE', @@ -107,7 +107,7 @@ export const darkTheme = createTheme({ }, border: '#E6E6E6', textContrast: '#FFFFFF', - textVerySubtle: '#DDD', + textVerySubtle: '#ececec', textSubtle: '#EEEEEE', highlight: '#FFFBCC', errorBackground: '#FFEBEE', diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 378ceb2d08..b9e37d6d7a 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,10 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/plugin-catalog": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/plugin-catalog": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@kyma-project/asyncapi-react": "^0.11.0", "@material-icons/font": "^1.0.2", "@material-ui/core": "^4.11.0", @@ -39,9 +39,9 @@ "swagger-ui-react": "^3.31.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", - "@backstage/test-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", + "@backstage/test-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", @@ -49,7 +49,9 @@ "@types/node": "^12.0.0", "@types/react": "^16.9", "@types/swagger-ui-react": "^3.23.3", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index c4738c7105..d403580f86 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.22", - "@backstage/config-loader": "^0.1.1-alpha.22", + "@backstage/backend-common": "^0.1.1-alpha.23", + "@backstage/config-loader": "^0.1.1-alpha.23", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", @@ -30,9 +30,9 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", "@types/supertest": "^2.0.8", - "msw": "^0.19.5", + "msw": "^0.20.5", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/auth-backend/README.md b/plugins/auth-backend/README.md index 9af90df3f2..355fcc1536 100644 --- a/plugins/auth-backend/README.md +++ b/plugins/auth-backend/README.md @@ -34,8 +34,8 @@ Follow this link, [Create new OAuth App](https://github.com/settings/application 1. Set Application Name to `backstage-dev` or something along those lines. 1. You can set the Homepage URL to whatever you want to. 1. The Authorization Callback URL should match the redirect URI set in Backstage. - 1. Set this to `http://localhost:7000/auth/github` for local development. - 1. Set this to `http://{APP_FQDN}:{APP_BACKEND_PORT}/auth/github` for non-local deployments. + 1. Set this to `http://localhost:7000/api/auth/github` for local development. + 1. Set this to `http://{APP_FQDN}:{APP_BACKEND_PORT}/api/auth/github` for non-local deployments. ```bash export AUTH_GITHUB_CLIENT_ID=x @@ -78,14 +78,14 @@ export AUTH_AUTH0_CLIENT_SECRET=x #### Creating an Azure AD App Registration -An Azure AD App Registration is required to be able to sign in using Azure AD and the Microsoft Graph API. +An Azure AD App Registration is required to be able to sign in using Azure AD and the Microsoft Graph API. Click [here](https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps) to create a new one. - Click on the `New Registration` button. - Give the app a name. e.g. `backstage-dev` - Select `Accounts in this organizational directory only` under supported account types. - Enter the callback URL for your backstage backend instance: - - For local development, this is likely `http://localhost:7000/auth/microsoft/handler/frame` + - For local development, this is likely `http://localhost:7000/api/auth/microsoft/handler/frame` - For non-local deployments, this will be `https://{APP_FQDN}:{APP_BACKEND_PORT}/auth/microsoft/handler/frame` - Click `Register`. diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 91fe08c0fa..8a9bfadca1 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-backend", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.22", - "@backstage/config": "^0.1.1-alpha.22", + "@backstage/backend-common": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", "@types/express": "^4.17.6", "compression": "^1.7.4", "cookie-parser": "^1.4.5", @@ -49,7 +49,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/jwt-decode": "2.2.1", @@ -58,7 +58,8 @@ "@types/passport-google-oauth20": "^2.0.3", "@types/passport-microsoft": "^0.0.0", "@types/passport-saml": "^1.1.2", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist", diff --git a/plugins/auth-backend/scripts/start-saml-idp.sh b/plugins/auth-backend/scripts/start-saml-idp.sh index 33217f7978..b312a4b85e 100755 --- a/plugins/auth-backend/scripts/start-saml-idp.sh +++ b/plugins/auth-backend/scripts/start-saml-idp.sh @@ -18,4 +18,4 @@ fi echo "Downloading and starting SAML-IdP" export NPM_CONFIG_REGISTRY=https://registry.npmjs.org -exec npx saml-idp --acsUrl "http://localhost:7000/auth/saml/handler/frame" --audience "http://localhost:7000" --port 7001 +exec npx saml-idp --acsUrl "http://localhost:7000/api/auth/saml/handler/frame" --audience "http://localhost:7000" --port 7001 diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 19a74d47c5..e70e687879 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -28,6 +28,7 @@ export interface RouterOptions { logger: Logger; database: Knex; config: Config; + basePath?: string; } export async function createRouter( @@ -38,7 +39,8 @@ export async function createRouter( const appUrl = options.config.getString('app.baseUrl'); const backendUrl = options.config.getString('backend.baseUrl'); - const authUrl = `${backendUrl}/auth`; + // TODO(Rugvip): Replace with service discovery of external URL + const authUrl = backendUrl + (options.basePath ?? '/api/auth'); const keyDurationSeconds = 3600; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index f443ff362f..7e3134a012 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.22", - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/config": "^0.1.1-alpha.22", + "@backstage/backend-common": "^0.1.1-alpha.23", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", "@types/express": "^4.17.6", "express": "^4.17.1", "express-promise-router": "^3.0.3", @@ -40,7 +40,7 @@ "yup": "^0.29.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", "@types/git-url-parse": "^9.0.0", "@types/lodash": "^4.14.151", "@types/node-fetch": "^2.5.7", diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index c508215f29..8597f3aa3c 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graphql", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/backend-common": "^0.1.1-alpha.22", - "@backstage/config": "^0.1.1-alpha.22", + "@backstage/backend-common": "^0.1.1-alpha.23", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", "@graphql-modules/core": "^0.7.17", "apollo-server": "^2.16.1", "graphql": "^15.3.0", @@ -32,16 +32,16 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@types/express": "^4.17.7", - "@types/supertest": "^2.0.8", + "@backstage/cli": "^0.1.1-alpha.23", "@graphql-codegen/cli": "^1.17.7", "@graphql-codegen/typescript": "^1.17.7", "@graphql-codegen/typescript-resolvers": "^1.17.7", - "ts-node": "^8.10.2", + "@types/express": "^4.17.7", + "@types/supertest": "^2.0.8", "eslint-plugin-graphql": "^4.0.0", - "msw": "^0.19.5", - "supertest": "^4.0.2" + "msw": "^0.20.5", + "supertest": "^4.0.2", + "ts-node": "^8.10.2" }, "files": [ "dist" diff --git a/plugins/catalog-graphql/src/graphql/module.ts b/plugins/catalog-graphql/src/graphql/module.ts index 06b2ea3f2b..be5fed11ef 100644 --- a/plugins/catalog-graphql/src/graphql/module.ts +++ b/plugins/catalog-graphql/src/graphql/module.ts @@ -22,6 +22,7 @@ import { Config } from '@backstage/config'; import { CatalogClient } from '../service/client'; import GraphQLJSON, { GraphQLJSONObject } from 'graphql-type-json'; import { Entity } from '@backstage/catalog-model'; +import typeDefs from '../schema'; export interface ModuleOptions { logger: Logger; @@ -31,8 +32,6 @@ export interface ModuleOptions { export async function createModule( options: ModuleOptions, ): Promise { - const { default: typeDefs } = require('../schema'); - const catalogClient = new CatalogClient( options.config.getString('backend.baseUrl'), ); diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index f5520f39f9..d3241ec10d 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/plugin-scaffolder": "^0.1.1-alpha.22", - "@backstage/plugin-techdocs": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/plugin-scaffolder": "^0.1.1-alpha.23", + "@backstage/plugin-techdocs": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -41,9 +41,9 @@ "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", - "@backstage/test-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", + "@backstage/test-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/react-hooks": "^3.3.0", @@ -53,7 +53,8 @@ "jest-fetch-mock": "^3.0.3", "msw": "^0.20.5", "react-test-renderer": "^16.13.1", - "whatwg-fetch": "^3.4.0" + "whatwg-fetch": "^3.4.0", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 10278fc065..a661c2562f 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/plugin-catalog": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/plugin-catalog": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -38,15 +38,17 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/react-lazylog": "^4.5.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/explore/package.json b/plugins/explore/package.json index c889f96153..2f25b4f137 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -33,15 +33,17 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", - "@backstage/test-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", + "@backstage/test-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index b71cebf6e4..406b2428df 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,10 +1,9 @@ { "name": "@backstage/plugin-gcp-projects", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, "publishConfig": { "access": "public", "main": "dist/index.esm.js", @@ -21,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,14 +31,16 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 9d1538ce99..5d85d8b3fa 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-actions", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/core-api": "^0.1.1-alpha.22", - "@backstage/plugin-catalog": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/core-api": "^0.1.1-alpha.23", + "@backstage/plugin-catalog": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -40,14 +40,16 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index cb25167ba6..a58ba6576a 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gitops-profiles", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,14 +32,16 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index e6122eb38b..1db4bba978 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "private": false, "publishConfig": { "access": "public", @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -43,9 +43,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", - "@backstage/test-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", + "@backstage/test-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", @@ -53,7 +53,9 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "jest-fetch-mock": "^3.0.3", - "react-router-dom": "6.0.0-beta.0" + "react-router-dom": "6.0.0-beta.0", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/graphql/package.json b/plugins/graphql/package.json index 9ba972bd22..f008e6f51e 100644 --- a/plugins/graphql/package.json +++ b/plugins/graphql/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-graphql-backend", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,10 +19,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.22", - "@backstage/plugin-catalog-graphql": "^0.1.1-alpha.22", + "@backstage/backend-common": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", + "@backstage/plugin-catalog-graphql": "^0.1.1-alpha.23", "@graphql-modules/core": "^0.7.17", - "@backstage/backend-common": "^0.1.1-alpha.22", "@types/express": "^4.17.6", "apollo-server": "^2.16.1", "apollo-server-express": "^2.16.1", @@ -36,7 +36,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", "@types/supertest": "^2.0.8", "eslint-plugin-graphql": "^4.0.0", "msw": "^0.20.5", diff --git a/plugins/identity-backend/package.json b/plugins/identity-backend/package.json index a34049fb91..63ecb65d1e 100644 --- a/plugins/identity-backend/package.json +++ b/plugins/identity-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-identity-backend", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.22", + "@backstage/backend-common": "^0.1.1-alpha.23", "@types/express": "^4.17.6", "compression": "^1.7.4", "cors": "^2.8.5", @@ -33,8 +33,9 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "jest-fetch-mock": "^3.0.3" + "@backstage/cli": "^0.1.1-alpha.23", + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5" }, "files": [ "dist" diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 398161f270..84e8a47664 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/plugin-catalog": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/plugin-catalog": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -36,15 +36,17 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/testing-library__jest-dom": "^5.9.1", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/kubernetes-backend/examples/dice-roller/README.md b/plugins/kubernetes-backend/examples/dice-roller/README.md new file mode 100644 index 0000000000..b64e2df37f --- /dev/null +++ b/plugins/kubernetes-backend/examples/dice-roller/README.md @@ -0,0 +1,42 @@ +# Dice roller + +An app to roll dice (it doesn't actually do that). + +# Viewing in local Minikube running Backstage locally + +## Prerequisites + +- kubectl installed +- Minikube installed +- jq installed +- Backstage locally built and ready to run + +## Steps + +1. Start minikube +2. Get the Kubernetes master base url `kubectl cluster-info` +3. Apply manifests `kubectl apply -f dice-roller-manifests.yaml` +4. Get service account token (see below) +5. Start Backstage UI and backend +6. Register existing component in Backstage + - https://github.com/mclarke47/dice-roller/blob/master/catalog-info.yaml + +Update `app-config.yaml` as follows. + +```yaml +--- +kubernetes: + clusterLocatorMethod: 'configMultiTenant' + clusters: + - url: + name: minikube + serviceAccountToken: +``` + +### Getting the service account token + +``` +kubectl get secret DICE_ROLLER_TOKEN_NAME -o=json | jq -r '.data["token"]' | base64 --decode | pbcopy +``` + +Paste into `app-config.yaml` `kubernetes.clusters[].serviceAccountToken` diff --git a/plugins/kubernetes-backend/examples/dice-roller/catalog-info.yaml b/plugins/kubernetes-backend/examples/dice-roller/catalog-info.yaml new file mode 100644 index 0000000000..a944232224 --- /dev/null +++ b/plugins/kubernetes-backend/examples/dice-roller/catalog-info.yaml @@ -0,0 +1,13 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: dice-roller + description: It rolls dice + tags: + - go + annotations: + 'backstage.io/kubernetes-id': dice-roller +spec: + type: service + lifecycle: production + owner: guest diff --git a/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml new file mode 100644 index 0000000000..ef448c1688 --- /dev/null +++ b/plugins/kubernetes-backend/examples/dice-roller/dice-roller-manifests.yaml @@ -0,0 +1,79 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dice-roller + labels: + 'backstage.io/kubernetes-id': dice-roller +spec: + selector: + matchLabels: + app: dice-roller + replicas: 2 + template: + metadata: + labels: + app: dice-roller + 'backstage.io/kubernetes-id': dice-roller + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: dice-roller + namespace: default + labels: + 'backstage.io/kubernetes-id': dice-roller +data: + foo: bar + +--- +apiVersion: v1 +kind: Secret +metadata: + name: dice-roller + labels: + 'backstage.io/kubernetes-id': dice-roller +type: Opaque +data: + username: YWRtaW4= +--- +apiVersion: v1 +kind: Service +metadata: + name: dice-roller + labels: + 'backstage.io/kubernetes-id': dice-roller +spec: + selector: + app: dice-roller + ports: + - protocol: TCP + port: 80 + targetPort: 9376 + +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: dice-roller +automountServiceAccountToken: false + +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: sa-admin +roleRef: + kind: ClusterRole + name: cluster-admin + apiGroup: rbac.authorization.k8s.io +subjects: + - kind: ServiceAccount + name: dice-roller + namespace: default diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 2e59aaeec0..e1f7a39592 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.22", + "@backstage/backend-common": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", "@types/express": "^4.17.6", + "@kubernetes/client-node": "^0.12.1", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", @@ -29,12 +31,14 @@ "fs-extra": "^9.0.0", "helmet": "^4.0.0", "morgan": "^1.10.0", + "stream-buffers": "^3.0.2", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "supertest": "^4.0.2", + "@backstage/cli": "^0.1.1-alpha.23" }, "files": [ "dist" diff --git a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts new file mode 100644 index 0000000000..34788d16cb --- /dev/null +++ b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.test.ts @@ -0,0 +1,105 @@ +/* + * 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 '@backstage/backend-common'; +import { MultiTenantConfigClusterLocator } from './MultiTenantConfigClusterLocator'; +import { ConfigReader, Config } from '@backstage/config'; + +describe('MultiTenantConfigClusterLocator', () => { + it('empty clusters returns empty cluster details', async () => { + const config: Config = new ConfigReader( + { + clusters: [], + }, + 'ctx', + ); + + const sut = MultiTenantConfigClusterLocator.fromConfig( + config.getConfigArray('clusters'), + ); + + const result = await sut.getClusterByServiceId('ignored'); + + expect(result).toStrictEqual([]); + }); + + it('one clusters returns one cluster details', async () => { + const config: Config = new ConfigReader( + { + clusters: [ + { + name: 'cluster1', + url: 'http://localhost:8080', + }, + ], + }, + 'ctx', + ); + + const sut = MultiTenantConfigClusterLocator.fromConfig( + config.getConfigArray('clusters'), + ); + + const result = await sut.getClusterByServiceId('ignored'); + + expect(result).toStrictEqual([ + { + name: 'cluster1', + serviceAccountToken: undefined, + url: 'http://localhost:8080', + }, + ]); + }); + + it('two clusters returns two cluster details', async () => { + const config: Config = new ConfigReader( + { + clusters: [ + { + name: 'cluster1', + serviceAccountToken: undefined, + url: 'http://localhost:8080', + }, + { + name: 'cluster2', + serviceAccountToken: undefined, + url: 'http://localhost:8081', + }, + ], + }, + 'ctx', + ); + + const sut = MultiTenantConfigClusterLocator.fromConfig( + config.getConfigArray('clusters'), + ); + + const result = await sut.getClusterByServiceId('ignored'); + + expect(result).toStrictEqual([ + { + name: 'cluster1', + serviceAccountToken: undefined, + url: 'http://localhost:8080', + }, + { + name: 'cluster2', + serviceAccountToken: undefined, + url: 'http://localhost:8081', + }, + ]); + }); +}); diff --git a/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.ts new file mode 100644 index 0000000000..3d9e2dd8b4 --- /dev/null +++ b/plugins/kubernetes-backend/src/cluster-locator/MultiTenantConfigClusterLocator.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 { Config } from '@backstage/config'; +import { ClusterDetails, KubernetesClusterLocator } from '..'; + +// This cluster locator assumes that every service is located on every cluster +// Therefore it will always return all clusters in an app configuration file +export class MultiTenantConfigClusterLocator + implements KubernetesClusterLocator { + private readonly clusterDetails: ClusterDetails[]; + + constructor(clusterDetails: ClusterDetails[]) { + this.clusterDetails = clusterDetails; + } + + static fromConfig(config: Config[]): MultiTenantConfigClusterLocator { + return new MultiTenantConfigClusterLocator( + config.map(c => { + return { + name: c.getString('name'), + url: c.getString('url'), + serviceAccountToken: c.getOptionalString('serviceAccountToken'), + }; + }), + ); + } + + // As this implementation always returns all clusters serviceId is ignored here + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getClusterByServiceId(_serviceId: string): Promise { + return this.clusterDetails; + } +} diff --git a/plugins/kubernetes-backend/src/cluster-locator/types.ts b/plugins/kubernetes-backend/src/cluster-locator/types.ts new file mode 100644 index 0000000000..9b793da558 --- /dev/null +++ b/plugins/kubernetes-backend/src/cluster-locator/types.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export type ClusterLocatorMethod = 'configMultiTenant' | 'http'; diff --git a/plugins/kubernetes-backend/src/index.ts b/plugins/kubernetes-backend/src/index.ts index 7612c392a2..96f070ff1e 100644 --- a/plugins/kubernetes-backend/src/index.ts +++ b/plugins/kubernetes-backend/src/index.ts @@ -15,3 +15,4 @@ */ export * from './service/router'; +export * from './types/types'; diff --git a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts new file mode 100644 index 0000000000..a3dc67716a --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.test.ts @@ -0,0 +1,136 @@ +/* + * 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 '@backstage/backend-common'; +import { KubernetesClientProvider } from './KubernetesClientProvider'; + +describe('KubernetesClientProvider', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('can get core client by cluster details', async () => { + const sut = new KubernetesClientProvider(); + + const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({})); + + sut.getKubeConfig = mockGetKubeConfig; + + const result = sut.getCoreClientByClusterDetails({ + name: 'cluster-name', + url: 'http://localhost:9999', + serviceAccountToken: 'TOKEN', + }); + + expect(result.basePath).toBe('http://localhost:9999'); + // These fields aren't on the type but are there + const auth = (result as any).authentications.default; + expect(auth.users[0].token).toBe('TOKEN'); + expect(auth.clusters[0].name).toBe('cluster-name'); + + expect(mockGetKubeConfig.mock.calls.length).toBe(1); + }); + + it('can get cached core client by cluster details', async () => { + const sut = new KubernetesClientProvider(); + + const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({})); + + sut.getKubeConfig = mockGetKubeConfig; + + const result1 = sut.getCoreClientByClusterDetails({ + name: 'cluster-name', + url: 'http://localhost:9999', + serviceAccountToken: 'TOKEN', + }); + + const result2 = sut.getCoreClientByClusterDetails({ + name: 'cluster-name', + url: 'http://localhost:9999', + serviceAccountToken: 'TOKEN', + }); + + expect(result1.basePath).toBe('http://localhost:9999'); + // These fields aren't on the type but are there + const auth1 = (result1 as any).authentications.default; + expect(auth1.users[0].token).toBe('TOKEN'); + expect(auth1.clusters[0].name).toBe('cluster-name'); + + expect(result2.basePath).toBe('http://localhost:9999'); + // These fields aren't on the type but are there + const auth2 = (result2 as any).authentications.default; + expect(auth2.users[0].token).toBe('TOKEN'); + expect(auth2.clusters[0].name).toBe('cluster-name'); + + expect(mockGetKubeConfig.mock.calls.length).toBe(1); + }); + + it('can get apps client by cluster details', async () => { + const sut = new KubernetesClientProvider(); + + const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({})); + + sut.getKubeConfig = mockGetKubeConfig; + + const result = sut.getAppsClientByClusterDetails({ + name: 'cluster-name', + url: 'http://localhost:9999', + serviceAccountToken: 'TOKEN', + }); + + expect(result.basePath).toBe('http://localhost:9999'); + // These fields aren't on the type but are there + const auth = (result as any).authentications.default; + expect(auth.users[0].token).toBe('TOKEN'); + expect(auth.clusters[0].name).toBe('cluster-name'); + + expect(mockGetKubeConfig.mock.calls.length).toBe(1); + }); + + it('can get cached apps client by cluster details', async () => { + const sut = new KubernetesClientProvider(); + + const mockGetKubeConfig = jest.fn(sut.getKubeConfig.bind({})); + + sut.getKubeConfig = mockGetKubeConfig; + + const result1 = sut.getAppsClientByClusterDetails({ + name: 'cluster-name', + url: 'http://localhost:9999', + serviceAccountToken: 'TOKEN', + }); + + const result2 = sut.getAppsClientByClusterDetails({ + name: 'cluster-name', + url: 'http://localhost:9999', + serviceAccountToken: 'TOKEN', + }); + + expect(result1.basePath).toBe('http://localhost:9999'); + // These fields aren't on the type but are there + const auth1 = (result1 as any).authentications.default; + expect(auth1.users[0].token).toBe('TOKEN'); + expect(auth1.clusters[0].name).toBe('cluster-name'); + + expect(result2.basePath).toBe('http://localhost:9999'); + // These fields aren't on the type but are there + const auth2 = (result2 as any).authentications.default; + expect(auth2.users[0].token).toBe('TOKEN'); + expect(auth2.clusters[0].name).toBe('cluster-name'); + + expect(mockGetKubeConfig.mock.calls.length).toBe(1); + }); +}); diff --git a/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts new file mode 100644 index 0000000000..c3986e3ff7 --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesClientProvider.ts @@ -0,0 +1,96 @@ +/* + * 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 { ClusterDetails } from '..'; +import { AppsV1Api, CoreV1Api, KubeConfig } from '@kubernetes/client-node'; + +export class KubernetesClientProvider { + private readonly coreClientMap: { + [key: string]: CoreV1Api; + }; + + private readonly appsClientMap: { + [key: string]: AppsV1Api; + }; + + constructor() { + this.coreClientMap = {}; + this.appsClientMap = {}; + } + + // visible for testing + getKubeConfig(clusterDetails: ClusterDetails) { + const cluster = { + name: clusterDetails.name, + server: clusterDetails.url, + // TODO configure this + skipTLSVerify: true, + }; + + // TODO configure + const user = { + name: 'service-account', + token: clusterDetails.serviceAccountToken, + }; + + const context = { + name: `${clusterDetails.name}`, + user: user.name, + cluster: cluster.name, + }; + + const kc = new KubeConfig(); + kc.loadFromOptions({ + clusters: [cluster], + users: [user], + contexts: [context], + currentContext: context.name, + }); + return kc; + } + + getCoreClientByClusterDetails(clusterDetails: ClusterDetails) { + const clientMapKey = clusterDetails.name; + + if (this.coreClientMap.hasOwnProperty(clientMapKey)) { + return this.coreClientMap[clientMapKey]; + } + + const kc = this.getKubeConfig(clusterDetails); + + const client = kc.makeApiClient(CoreV1Api); + + this.coreClientMap[clientMapKey] = client; + + return client; + } + + getAppsClientByClusterDetails(clusterDetails: ClusterDetails) { + const clientMapKey = clusterDetails.name; + + if (this.appsClientMap.hasOwnProperty(clientMapKey)) { + return this.appsClientMap[clientMapKey]; + } + + const kc = this.getKubeConfig(clusterDetails); + + const client = kc.makeApiClient(AppsV1Api); + + this.appsClientMap[clientMapKey] = client; + + return client; + } +} diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts new file mode 100644 index 0000000000..359e7c5806 --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.test.ts @@ -0,0 +1,108 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; + +describe('KubernetesClientProvider', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should return pods, services', async () => { + const clientMock: any = { + listPodForAllNamespaces: jest.fn(), + listServiceForAllNamespaces: jest.fn(), + }; + + const kubernetesClientProvider: any = { + getCoreClientByClusterDetails: jest.fn(() => clientMock), + getAppsClientByClusterDetails: jest.fn(() => clientMock), + }; + + const sut = new KubernetesClientBasedFetcher({ + kubernetesClientProvider, + logger: getVoidLogger(), + }); + + clientMock.listPodForAllNamespaces.mockResolvedValueOnce({ + body: { + items: [ + { + metadata: { + name: 'pod-name', + }, + }, + ], + }, + }); + + clientMock.listServiceForAllNamespaces.mockResolvedValueOnce({ + body: { + items: [ + { + metadata: { + name: 'service-name', + }, + }, + ], + }, + }); + + const result = await sut.fetchObjectsByServiceId( + 'some-service', + { + name: 'cluster1', + url: 'http://localhost:9999', + serviceAccountToken: undefined, + }, + new Set(['pods', 'services']), + ); + + expect(result).toStrictEqual([ + { + type: 'pods', + resources: [ + { + metadata: { + name: 'pod-name', + }, + }, + ], + }, + { + type: 'services', + resources: [ + { + metadata: { + name: 'service-name', + }, + }, + ], + }, + ]); + + expect(clientMock.listPodForAllNamespaces.mock.calls.length).toBe(1); + expect(clientMock.listServiceForAllNamespaces.mock.calls.length).toBe(1); + + expect( + kubernetesClientProvider.getAppsClientByClusterDetails.mock.calls.length, + ).toBe(2); + expect( + kubernetesClientProvider.getCoreClientByClusterDetails.mock.calls.length, + ).toBe(2); + }); +}); diff --git a/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts new file mode 100644 index 0000000000..f6d3edb5e9 --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesFetcher.ts @@ -0,0 +1,212 @@ +/* + * 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 { + AppsV1Api, + CoreV1Api, + V1ConfigMap, + V1Deployment, + V1Pod, + V1ReplicaSet, + V1Secret, +} from '@kubernetes/client-node'; +import { KubernetesClientProvider } from './KubernetesClientProvider'; +import { V1Service } from '@kubernetes/client-node/dist/gen/model/v1Service'; +import { Logger } from 'winston'; +import { + KubernetesFetcher, + ClusterDetails, + KubernetesObjectTypes, + FetchResponse, +} from '..'; + +export interface Clients { + core: CoreV1Api; + apps: AppsV1Api; +} + +export interface KubernetesClientBasedFetcherOptions { + kubernetesClientProvider: KubernetesClientProvider; + logger: Logger; +} + +export class KubernetesClientBasedFetcher implements KubernetesFetcher { + private readonly kubernetesClientProvider: KubernetesClientProvider; + private readonly logger: Logger; + + constructor({ + kubernetesClientProvider, + logger, + }: KubernetesClientBasedFetcherOptions) { + this.kubernetesClientProvider = kubernetesClientProvider; + this.logger = logger; + } + + fetchObjectsByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + objectTypesToFetch: Set, + ): Promise { + return Promise.all( + Array.from(objectTypesToFetch).map(type => { + return this.fetchByObjectType(serviceId, clusterDetails, type); + }), + ); + } + + private fetchByObjectType( + serviceId: string, + clusterDetails: ClusterDetails, + type: KubernetesObjectTypes, + ): Promise { + switch (type) { + case 'pods': + return this.fetchPodsByServiceId(serviceId, clusterDetails).then(r => ({ + type: type, + resources: r, + })); + case 'configmaps': + return this.fetchConfigMapsByServiceId( + serviceId, + clusterDetails, + ).then(r => ({ type: type, resources: r })); + case 'deployments': + return this.fetchDeploymentsByServiceId( + serviceId, + clusterDetails, + ).then(r => ({ type: type, resources: r })); + case 'replicasets': + return this.fetchReplicaSetsByServiceId( + serviceId, + clusterDetails, + ).then(r => ({ type: type, resources: r })); + case 'secrets': + return this.fetchSecretsByServiceId( + serviceId, + clusterDetails, + ).then(r => ({ type: type, resources: r })); + case 'services': + return this.fetchServicesByServiceId( + serviceId, + clusterDetails, + ).then(r => ({ type: type, resources: r })); + default: + // unrecognised type + throw new Error(`unrecognised type=${type}`); + } + } + + private singleClusterFetch( + clusterDetails: ClusterDetails, + fn: (client: Clients) => Promise<{ body: { items: Array } }>, + ): Promise> { + const core = this.kubernetesClientProvider.getCoreClientByClusterDetails( + clusterDetails, + ); + const apps = this.kubernetesClientProvider.getAppsClientByClusterDetails( + clusterDetails, + ); + + this.logger.debug(`calling cluster=${clusterDetails.name}`); + return fn({ core, apps }).then(result => { + return result.body.items; + }); + } + + private fetchServicesByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + ): Promise> { + return this.singleClusterFetch(clusterDetails, ({ core }) => + core.listServiceForAllNamespaces( + false, + '', + '', + `backstage.io/kubernetes-id=${serviceId}`, + ), + ); + } + + private fetchPodsByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + ): Promise> { + return this.singleClusterFetch(clusterDetails, ({ core }) => + core.listPodForAllNamespaces( + false, + '', + '', + `backstage.io/kubernetes-id=${serviceId}`, + ), + ); + } + + private fetchConfigMapsByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + ): Promise> { + return this.singleClusterFetch(clusterDetails, ({ core }) => + core.listConfigMapForAllNamespaces( + false, + '', + '', + `backstage.io/kubernetes-id=${serviceId}`, + ), + ); + } + + private fetchSecretsByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + ): Promise> { + return this.singleClusterFetch(clusterDetails, ({ core }) => + core.listSecretForAllNamespaces( + false, + '', + '', + `backstage.io/kubernetes-id=${serviceId}`, + ), + ); + } + + private fetchDeploymentsByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + ): Promise> { + return this.singleClusterFetch(clusterDetails, ({ apps }) => + apps.listDeploymentForAllNamespaces( + false, + '', + '', + `backstage.io/kubernetes-id=${serviceId}`, + ), + ); + } + + private fetchReplicaSetsByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + ): Promise> { + return this.singleClusterFetch(clusterDetails, ({ apps }) => + apps.listReplicaSetForAllNamespaces( + false, + '', + '', + `backstage.io/kubernetes-id=${serviceId}`, + ), + ); + } +} diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts new file mode 100644 index 0000000000..3f8081571a --- /dev/null +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts @@ -0,0 +1,242 @@ +/* + * 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 { handleGetKubernetesObjectsByServiceId } from './getKubernetesObjectsByServiceIdHandler'; +import { getVoidLogger } from '@backstage/backend-common'; +import { ClusterDetails } from '..'; + +const TEST_SERVICE_ID = 'my-service'; + +const fetchObjectsByServiceId = jest.fn(); + +const getClusterByServiceId = jest.fn(); + +const mockFetch = (mock: jest.Mock) => { + mock.mockImplementation((serviceId: string, clusterDetails: ClusterDetails) => + Promise.resolve([ + { + type: 'pods', + resources: [ + { + metadata: { + name: `my-pods-${serviceId}-${clusterDetails.name}`, + }, + }, + ], + }, + { + type: 'configmaps', + resources: [ + { + metadata: { + name: `my-configmaps-${serviceId}-${clusterDetails.name}`, + }, + }, + ], + }, + { + type: 'services', + resources: [ + { + metadata: { + name: `my-services-${serviceId}-${clusterDetails.name}`, + }, + }, + ], + }, + ]), + ); +}; + +describe('handleGetKubernetesObjectsByServiceId', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('retrieve objects for one cluster', async () => { + getClusterByServiceId.mockImplementation(() => + Promise.resolve([ + { + name: 'test-cluster', + }, + ]), + ); + + mockFetch(fetchObjectsByServiceId); + + const result = await handleGetKubernetesObjectsByServiceId( + TEST_SERVICE_ID, + { + fetchObjectsByServiceId, + }, + { + getClusterByServiceId, + }, + getVoidLogger(), + ); + + expect(getClusterByServiceId.mock.calls.length).toBe(1); + expect(fetchObjectsByServiceId.mock.calls.length).toBe(1); + expect(result).toStrictEqual({ + items: [ + { + cluster: { + name: 'test-cluster', + }, + resources: [ + { + resources: [ + { + metadata: { + name: 'my-pods-my-service-test-cluster', + }, + }, + ], + type: 'pods', + }, + { + resources: [ + { + metadata: { + name: 'my-configmaps-my-service-test-cluster', + }, + }, + ], + type: 'configmaps', + }, + { + resources: [ + { + metadata: { + name: 'my-services-my-service-test-cluster', + }, + }, + ], + type: 'services', + }, + ], + }, + ], + }); + }); + + it('retrieve objects for two clusters', async () => { + getClusterByServiceId.mockImplementation(() => + Promise.resolve([ + { + name: 'test-cluster', + }, + { + name: 'other-cluster', + }, + ]), + ); + + mockFetch(fetchObjectsByServiceId); + + const result = await handleGetKubernetesObjectsByServiceId( + TEST_SERVICE_ID, + { + fetchObjectsByServiceId, + }, + { + getClusterByServiceId, + }, + getVoidLogger(), + ); + + expect(getClusterByServiceId.mock.calls.length).toBe(1); + expect(fetchObjectsByServiceId.mock.calls.length).toBe(2); + expect(result).toStrictEqual({ + items: [ + { + cluster: { + name: 'test-cluster', + }, + resources: [ + { + resources: [ + { + metadata: { + name: 'my-pods-my-service-test-cluster', + }, + }, + ], + type: 'pods', + }, + { + resources: [ + { + metadata: { + name: 'my-configmaps-my-service-test-cluster', + }, + }, + ], + type: 'configmaps', + }, + { + resources: [ + { + metadata: { + name: 'my-services-my-service-test-cluster', + }, + }, + ], + type: 'services', + }, + ], + }, + { + cluster: { + name: 'other-cluster', + }, + resources: [ + { + resources: [ + { + metadata: { + name: 'my-pods-my-service-other-cluster', + }, + }, + ], + type: 'pods', + }, + { + resources: [ + { + metadata: { + name: 'my-configmaps-my-service-other-cluster', + }, + }, + ], + type: 'configmaps', + }, + { + resources: [ + { + metadata: { + name: 'my-services-my-service-other-cluster', + }, + }, + ], + type: 'services', + }, + ], + }, + ], + }); + }); +}); diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts new file mode 100644 index 0000000000..52968d4c20 --- /dev/null +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.ts @@ -0,0 +1,69 @@ +/* + * 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 { + KubernetesClusterLocator, + KubernetesFetcher, + KubernetesObjectTypes, + ObjectsByServiceIdResponse, +} from '..'; + +export type GetKubernetesObjectsByServiceIdHandler = ( + serviceId: string, + fetcher: KubernetesFetcher, + clusterLocator: KubernetesClusterLocator, + logger: Logger, + objectsToFetch?: Set, +) => Promise; + +const DEFAULT_OBJECTS = new Set([ + 'pods', + 'services', + 'configmaps', + 'secrets', + 'deployments', + 'replicasets', +]); + +export const handleGetKubernetesObjectsByServiceId: GetKubernetesObjectsByServiceIdHandler = async ( + serviceId, + fetcher, + clusterLocator, + logger, + objectsToFetch = DEFAULT_OBJECTS, +) => { + const clusterDetails = await clusterLocator.getClusterByServiceId(serviceId); + + logger.info( + `serviceId=${serviceId} clusterDetails=${clusterDetails.map(c => c.name)}`, + ); + + return Promise.all( + clusterDetails.map(cd => { + return fetcher + .fetchObjectsByServiceId(serviceId, cd, objectsToFetch) + .then(result => { + return { + cluster: { + name: cd.name, + }, + resources: result, + }; + }); + }), + ).then(r => ({ items: r })); +}; diff --git a/plugins/kubernetes-backend/src/service/router.test.ts b/plugins/kubernetes-backend/src/service/router.test.ts new file mode 100644 index 0000000000..7731fe9abb --- /dev/null +++ b/plugins/kubernetes-backend/src/service/router.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { getVoidLogger } from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; +import { makeRouter } from './router'; +import { + KubernetesClusterLocator, + KubernetesFetcher, + ObjectsByServiceIdResponse, +} from '..'; + +describe('router', () => { + let app: express.Express; + let kubernetesFetcher: jest.Mocked; + let kubernetesClusterLocator: jest.Mocked; + let handleGetByServiceId: jest.Mock>; + + beforeAll(async () => { + kubernetesFetcher = { + fetchObjectsByServiceId: jest.fn(), + }; + + kubernetesClusterLocator = { + getClusterByServiceId: jest.fn(), + }; + + handleGetByServiceId = jest.fn(); + + const router = makeRouter( + getVoidLogger(), + kubernetesFetcher, + kubernetesClusterLocator, + handleGetByServiceId as any, + ); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /services/:serviceId', () => { + it('happy path: lists kubernetes objects', async () => { + const result = { + clusterOne: { + pods: [ + { + metadata: { + name: 'pod1', + }, + }, + ], + }, + } as any; + handleGetByServiceId.mockReturnValueOnce(Promise.resolve(result)); + + const response = await request(app).get('/services/test-service'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(result); + }); + + it('internal error: lists kubernetes objects', async () => { + handleGetByServiceId.mockRejectedValue(Error('some internal error')); + + const response = await request(app).get('/services/test-service'); + + expect(response.status).toEqual(500); + expect(response.body).toEqual({ error: 'some internal error' }); + }); + }); +}); diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index b200896887..d038c1ed7d 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -17,19 +17,65 @@ import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; +import { Config } from '@backstage/config'; +import { ClusterLocatorMethod } from '../cluster-locator/types'; +import { MultiTenantConfigClusterLocator } from '../cluster-locator/MultiTenantConfigClusterLocator'; +import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; +import { KubernetesClientProvider } from './KubernetesClientProvider'; +import { + GetKubernetesObjectsByServiceIdHandler, + handleGetKubernetesObjectsByServiceId, +} from './getKubernetesObjectsByServiceIdHandler'; +import { KubernetesClusterLocator, KubernetesFetcher } from '..'; export interface RouterOptions { logger: Logger; + config: Config; } -const makeRouter = (logger: Logger): express.Router => { +const getClusterLocator = (config: Config): KubernetesClusterLocator => { + const clusterLocatorMethod = config.getString( + 'kubernetes.clusterLocatorMethod', + ) as ClusterLocatorMethod; + + switch (clusterLocatorMethod) { + case 'configMultiTenant': + return MultiTenantConfigClusterLocator.fromConfig( + config.getConfigArray('kubernetes.clusters'), + ); + case 'http': + throw new Error('not implemented'); + default: + throw new Error( + `Unsupported kubernetes.clusterLocatorMethod "${clusterLocatorMethod}"`, + ); + } +}; + +export const makeRouter = ( + logger: Logger, + fetcher: KubernetesFetcher, + clusterLocator: KubernetesClusterLocator, + handleGetByServiceId: GetKubernetesObjectsByServiceIdHandler, +): express.Router => { const router = Router(); router.use(express.json()); + // TODO error handling router.get('/services/:serviceId', async (req, res) => { const serviceId = req.params.serviceId; - logger.info(`HERE ${serviceId}`); - res.send({ serviceId }); + + try { + const response = await handleGetByServiceId( + serviceId, + fetcher, + clusterLocator, + logger, + ); + res.send(response); + } catch (e) { + res.status(500).send({ error: e.message }); + } }); return router; @@ -41,5 +87,18 @@ export async function createRouter( const logger = options.logger; logger.info('Initializing Kubernetes backend'); - return makeRouter(logger); + + const clusterLocator = getClusterLocator(options.config); + + const fetcher = new KubernetesClientBasedFetcher({ + kubernetesClientProvider: new KubernetesClientProvider(), + logger, + }); + + return makeRouter( + logger, + fetcher, + clusterLocator, + handleGetKubernetesObjectsByServiceId, + ); } diff --git a/plugins/kubernetes-backend/src/service/standaloneApplication.ts b/plugins/kubernetes-backend/src/service/standaloneApplication.ts index 5eea4fb8f5..e23e87a7a2 100644 --- a/plugins/kubernetes-backend/src/service/standaloneApplication.ts +++ b/plugins/kubernetes-backend/src/service/standaloneApplication.ts @@ -25,6 +25,7 @@ import express from 'express'; import helmet from 'helmet'; import { Logger } from 'winston'; import { createRouter } from './router'; +import { ConfigReader } from '@backstage/config'; export interface ApplicationOptions { enableCors: boolean; @@ -35,6 +36,7 @@ export async function createStandaloneApplication( options: ApplicationOptions, ): Promise { const { enableCors, logger } = options; + const config = ConfigReader.fromConfigs([]); const app = express(); app.use(helmet()); @@ -44,7 +46,7 @@ export async function createStandaloneApplication( app.use(compression()); app.use(express.json()); app.use(requestLoggingHandler()); - app.use('/', await createRouter({ logger })); + app.use('/', await createRouter({ logger, config })); app.use(notFoundHandler()); app.use(errorHandler()); diff --git a/plugins/kubernetes-backend/src/types/types.ts b/plugins/kubernetes-backend/src/types/types.ts new file mode 100644 index 0000000000..2c69fd67a0 --- /dev/null +++ b/plugins/kubernetes-backend/src/types/types.ts @@ -0,0 +1,103 @@ +/* + * 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 { + V1ConfigMap, + V1Deployment, + V1Pod, + V1ReplicaSet, + V1Secret, + V1Service, +} from '@kubernetes/client-node'; + +export interface ClusterDetails { + name: string; + url: string; + // TODO this will eventually be configured by the auth translation work + serviceAccountToken: string | undefined; +} + +export interface ClusterObjects { + cluster: { name: string }; + resources: FetchResponse[]; +} + +export interface ObjectsByServiceIdResponse { + items: ClusterObjects[]; +} + +export type FetchResponse = + | PodFetchResponse + | ServiceFetchResponse + | ConfigMapFetchResponse + | SecretFetchResponse + | DeploymentFetchResponse + | ReplicaSetsFetchResponse; + +// TODO fairly sure there's a easier way to do this + +export type KubernetesObjectTypes = + | 'pods' + | 'services' + | 'configmaps' + | 'secrets' + | 'deployments' + | 'replicasets'; + +export interface PodFetchResponse { + type: 'pods'; + resources: Array; +} + +export interface ServiceFetchResponse { + type: 'services'; + resources: Array; +} + +export interface ConfigMapFetchResponse { + type: 'configmaps'; + resources: Array; +} + +export interface SecretFetchResponse { + type: 'secrets'; + resources: Array; +} + +export interface DeploymentFetchResponse { + type: 'deployments'; + resources: Array; +} + +export interface ReplicaSetsFetchResponse { + type: 'replicasets'; + resources: Array; +} + +// Fetches information from a kubernetes cluster using the cluster details object +// to target a specific cluster +export interface KubernetesFetcher { + fetchObjectsByServiceId( + serviceId: string, + clusterDetails: ClusterDetails, + objectTypesToFetch: Set, + ): Promise; +} + +// Used to locate which cluster(s) a service is running on +export interface KubernetesClusterLocator { + getClusterByServiceId(serviceId: string): Promise; +} diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index aa8a7f61e4..d1f3d5e7ba 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,26 +20,29 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/plugin-kubernetes-backend": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", - "react-router-dom": "6.0.0-beta.0", "react-dom": "^16.13.1", + "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/kubernetes/src/Router.tsx b/plugins/kubernetes/src/Router.tsx index 994788bf72..41f534fbef 100644 --- a/plugins/kubernetes/src/Router.tsx +++ b/plugins/kubernetes/src/Router.tsx @@ -22,7 +22,7 @@ import { rootCatalogKubernetesRouteRef } from './plugin'; import { KubernetesContent } from './components/KubernetesContent'; import { WarningPanel } from '@backstage/core'; -const KUBERNETES_ANNOTATION = 'backstage.io/kubernetes'; +const KUBERNETES_ANNOTATION = 'backstage.io/kubernetes-id'; export const Router = ({ entity }: { entity: Entity }) => { const kubernetesAnnotationValue = diff --git a/plugins/kubernetes/src/api/KubernetesBackendClient.ts b/plugins/kubernetes/src/api/KubernetesBackendClient.ts index fed3cd2ed0..cbb3b03a38 100644 --- a/plugins/kubernetes/src/api/KubernetesBackendClient.ts +++ b/plugins/kubernetes/src/api/KubernetesBackendClient.ts @@ -16,6 +16,7 @@ import { DiscoveryApi } from '@backstage/core'; import { KubernetesApi } from './types'; +import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend'; export class KubernetesBackendClient implements KubernetesApi { private readonly discoveryApi: DiscoveryApi; @@ -37,7 +38,9 @@ export class KubernetesBackendClient implements KubernetesApi { return await response.json(); } - async getObjectsByServiceId(serviceId: String): Promise<{}> { + async getObjectsByServiceId( + serviceId: String, + ): Promise { return await this.getRequired(`/services/${serviceId}`); } } diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts index 2ad6e567f9..5ec3cda6f8 100644 --- a/plugins/kubernetes/src/api/types.ts +++ b/plugins/kubernetes/src/api/types.ts @@ -15,6 +15,7 @@ */ import { createApiRef } from '@backstage/core'; +import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend'; export const kubernetesApiRef = createApiRef({ id: 'plugin.kubernetes.service', @@ -23,5 +24,5 @@ export const kubernetesApiRef = createApiRef({ }); export interface KubernetesApi { - getObjectsByServiceId(serviceId: String): Promise<{}>; + getObjectsByServiceId(serviceId: String): Promise; } diff --git a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx index d79454a5d1..bbcef6a0d0 100644 --- a/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx +++ b/plugins/kubernetes/src/components/KubernetesContent/KubernetesContent.tsx @@ -15,25 +15,19 @@ */ import React, { FC, useEffect, useState } from 'react'; -import { Typography, Grid } from '@material-ui/core'; -import { - InfoCard, - Page, - pageTheme, - Content, - ContentHeader, - useApi, -} from '@backstage/core'; +import { Grid } from '@material-ui/core'; +import { InfoCard, Page, pageTheme, Content, useApi } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; import { kubernetesApiRef } from '../../api/types'; +import { ObjectsByServiceIdResponse } from '@backstage/plugin-kubernetes-backend'; // TODO this is a temporary component used to construct the Kubernetes plugin boilerplate export const KubernetesContent: FC<{ entity: Entity }> = ({ entity }) => { const kubernetesApi = useApi(kubernetesApiRef); - const [kubernetesObjects, setKubernetesObjects] = useState<{} | undefined>( - undefined, - ); + const [kubernetesObjects, setKubernetesObjects] = useState< + ObjectsByServiceIdResponse | undefined + >(undefined); const [error, setError] = useState(undefined); useEffect(() => { @@ -50,21 +44,28 @@ export const KubernetesContent: FC<{ entity: Entity }> = ({ entity }) => { return ( - - - - - {kubernetesObjects === undefined &&
loading....
} - {error !== undefined &&
{error}
} - {kubernetesObjects !== undefined && ( -
- backend response: {JSON.stringify(kubernetesObjects)} -
- )} -
-
-
+ {kubernetesObjects === undefined &&
loading....
} + {error !== undefined &&
{error}
} + {kubernetesObjects !== undefined && ( +
+ {kubernetesObjects.items.map((item, i) => ( + + + {item.resources.map((fr, j) => ( +
+
+ {fr.type}:{' '} + {(fr.resources as any) + .map((v: any) => v.metadata.name) + .join(' ')} +
+ ))} +
+
+ ))} +
+ )}
diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index c5522ae67a..6ca1495a05 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/config": "^0.1.1-alpha.22", - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/config": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -34,15 +34,17 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", - "@backstage/test-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", + "@backstage/test-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 0cadf440f7..38a5c437a4 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -31,14 +31,16 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index fb1b82f3ea..10024fcd42 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-proxy-backend", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -19,8 +19,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.22", - "@backstage/config": "^0.1.1-alpha.22", + "@backstage/backend-common": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", "@types/express": "^4.17.6", "@types/http-proxy-middleware": "^0.19.3", "express": "^4.17.1", @@ -35,7 +35,7 @@ "yup": "^0.29.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", "@types/node-fetch": "^2.5.7", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 8ccaae9914..9e3ef7e307 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-register-component", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/plugin-catalog": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/plugin-catalog": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -36,14 +36,16 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 969d3c79b9..2ed15b7aff 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar-backend", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.22", - "@backstage/config": "^0.1.1-alpha.22", + "@backstage/backend-common": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", "@types/express": "^4.17.6", "axios": "^0.20.0", "camelcase-keys": "^6.2.2", @@ -37,7 +37,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", "@types/supertest": "^2.0.8", "jest-fetch-mock": "^3.0.3", "supertest": "^4.0.2" diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index cb37c6187a..18f13c3969 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/plugin-catalog": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/plugin-catalog": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -37,9 +37,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", - "@backstage/test-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", + "@backstage/test-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/react-hooks": "^3.3.0", @@ -47,7 +47,9 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/react": "^16.9", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 0eaa33e1f8..90d1af4b5f 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.22", - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/config": "^0.1.1-alpha.22", + "@backstage/backend-common": "^0.1.1-alpha.23", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", "@gitbeaker/core": "^23.5.0", "@gitbeaker/node": "^23.5.0", "@octokit/rest": "^18.0.0", @@ -46,7 +46,7 @@ "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", "@octokit/types": "^5.4.1", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index ab95e83201..2abd3feb8f 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,10 +21,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/plugin-catalog": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/plugin-catalog": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -41,15 +41,17 @@ "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", - "@backstage/test-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", + "@backstage/test-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/sentry-backend/package.json b/plugins/sentry-backend/package.json index dbc7b48a1b..68b94ce7d8 100644 --- a/plugins/sentry-backend/package.json +++ b/plugins/sentry-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry-backend", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.22", + "@backstage/backend-common": "^0.1.1-alpha.23", "@types/express": "^4.17.6", "axios": "^0.20.0", "compression": "^1.7.4", @@ -34,7 +34,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 6e7daea15c..143a2dfc12 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -36,14 +36,16 @@ "timeago.js": "^4.0.2" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index f25ec752e1..c9950cdc95 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-radar", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,9 +21,9 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/test-utils": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/test-utils": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -35,8 +35,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", @@ -45,7 +45,9 @@ "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "@types/react": "^16.9", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/techdocs-backend/README.md b/plugins/techdocs-backend/README.md index 2185c3b3e4..b7eca2c5cb 100644 --- a/plugins/techdocs-backend/README.md +++ b/plugins/techdocs-backend/README.md @@ -18,11 +18,11 @@ yarn start ## What techdocs-backend does -This plugin is the backend part of the techdocs plugin. It provides building and serving of your docs without having to use another service and hosting provider. To use it set your techdocs storageUrl in your `app-config.yml` to `http://localhost:7000/techdocs/static/docs`. +This plugin is the backend part of the techdocs plugin. It provides building and serving of your docs without having to use another service and hosting provider. To use it set your techdocs storageUrl in your `app-config.yml` to `http://localhost:7000/api/techdocs/static/docs`. ```yaml techdocs: - storageUrl: http://localhost:7000/techdocs/static/docs + storageUrl: http://localhost:7000/api/techdocs/static/docs ``` ## Extending techdocs-backend diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index 1ca8b3ee44..0ff9722718 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.1.1-alpha.22", - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/config": "^0.1.1-alpha.22", + "@backstage/backend-common": "^0.1.1-alpha.23", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/config": "^0.1.1-alpha.23", "@types/dockerode": "^2.5.34", "@types/express": "^4.17.6", "command-exists-promise": "^2.0.2", @@ -38,7 +38,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", "@types/node-fetch": "^2.5.7", "supertest": "^4.0.2" }, diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index e0a066c168..14820da4e9 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -63,7 +63,7 @@ export async function createRouter({ const entity = (await ( await fetch( - `${baseUrl}/catalog/entities/by-name/${kind}/${namespace}/${name}`, + `${baseUrl}/api/catalog/entities/by-name/${kind}/${namespace}/${name}`, ) ).json()) as Entity; diff --git a/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts b/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts index e33c6fa17d..f328443965 100644 --- a/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts +++ b/plugins/techdocs-backend/src/techdocs/stages/publish/local.ts @@ -64,7 +64,7 @@ export class LocalPublish implements PublisherBase { } resolve({ - remoteUrl: `http://localhost:7000/techdocs/static/docs/${entity.metadata.name}`, + remoteUrl: `http://localhost:7000/api/techdocs/static/docs/${entity.metadata.name}`, }); }); }); diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index d2f99cb824..e376202a41 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -22,12 +22,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/core-api": "^0.1.1-alpha.22", - "@backstage/plugin-catalog": "^0.1.1-alpha.22", - "@backstage/test-utils": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/core-api": "^0.1.1-alpha.23", + "@backstage/plugin-catalog": "^0.1.1-alpha.23", + "@backstage/test-utils": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -40,15 +40,17 @@ "sanitize-html": "^1.27.0" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "canvas": "^2.6.1", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 8ae3273e12..2aa87ea1ee 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -21,6 +21,8 @@ import { useAsync } from 'react-use'; import { techdocsStorageApiRef } from '../../api'; import { useParams, useNavigate } from 'react-router-dom'; import { ParsedEntityId } from '../../types'; +import { useTheme } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; import transformer, { addBaseUrl, @@ -30,6 +32,7 @@ import transformer, { modifyCss, onCssReady, sanitizeDOM, + injectCss, } from '../transformers'; import { TechDocsNotFound } from './TechDocsNotFound'; @@ -40,6 +43,7 @@ type Props = { export const Reader = ({ entityId }: Props) => { const { kind, namespace, name } = entityId; const { '*': path } = useParams(); + const theme = useTheme(); const techdocsStorageApi = useApi(techdocsStorageApiRef); const [shadowDomRef, shadowRoot] = useShadowDom(); @@ -73,6 +77,18 @@ export const Reader = ({ entityId }: Props) => { }, }), removeMkdocsHeader(), + injectCss({ + css: ` + body { + font-family: ${theme.typography.fontFamily}; + --md-text-color: ${theme.palette.text.primary}; + --md-text-link-color: ${theme.palette.primary.main}; + + --md-code-fg-color: ${theme.palette.text.primary}; + --md-code-bg-color: ${theme.palette.background.paper}; + } + `, + }), ]); if (!transformedElement) { @@ -125,6 +141,7 @@ export const Reader = ({ entityId }: Props) => { entityId, navigate, techdocsStorageApi, + theme, ]); if (error) { diff --git a/plugins/techdocs/src/reader/transformers/index.ts b/plugins/techdocs/src/reader/transformers/index.ts index c8568282cf..ddbaadd071 100644 --- a/plugins/techdocs/src/reader/transformers/index.ts +++ b/plugins/techdocs/src/reader/transformers/index.ts @@ -21,6 +21,7 @@ export * from './removeMkdocsHeader'; export * from './modifyCss'; export * from './onCssReady'; export * from './sanitizeDOM'; +export * from './injectCss'; export type Transformer = (dom: Element) => Element; diff --git a/plugins/techdocs/src/reader/transformers/injectCss.test.ts b/plugins/techdocs/src/reader/transformers/injectCss.test.ts new file mode 100644 index 0000000000..afad9bbd2c --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/injectCss.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { createTestShadowDom } from '../../test-utils'; +import { injectCss } from '../transformers'; + +describe('injectCss', () => { + it('should inject style with passed css in head', () => { + const html = ` + + + + + `; + const injectedCss = '* {background-color: #fff}'; + + const shadowDom = createTestShadowDom(html, { + preTransformers: [injectCss({ css: injectedCss })], + postTransformers: [], + }); + + const styleElement = shadowDom.querySelector('head > style'); + + expect(styleElement).toBeTruthy(); + expect(styleElement!.innerHTML).toEqual(injectedCss); + }); +}); diff --git a/plugins/techdocs/src/reader/transformers/injectCss.ts b/plugins/techdocs/src/reader/transformers/injectCss.ts new file mode 100644 index 0000000000..2f6aa46bb6 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/injectCss.ts @@ -0,0 +1,31 @@ +/* + * 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 type { Transformer } from './index'; + +type InjectCssOptions = { + css: string; +}; + +export const injectCss = ({ css }: InjectCssOptions): Transformer => { + return dom => { + dom + .getElementsByTagName('head')[0] + .insertAdjacentHTML('beforeend', ``); + + return dom; + }; +}; diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 741a680cd9..d6c3a907b7 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-welcome", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -21,8 +21,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -32,14 +32,16 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" diff --git a/yarn.lock b/yarn.lock index 33a1720a14..46402a9815 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2441,6 +2441,34 @@ resolved "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== +"@kubernetes/client-node@^0.12.1": + version "0.12.2" + resolved "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.12.2.tgz#8728684bd57d1cbcbe14fe742e79be7021403eea" + integrity sha512-J0UwyFl1Iv/IZ6WMP7LaizBEoKPnqwtc8tIO2q/X+EuDT7eGpPPAMHXSEOC/EI9JGIf0FaJEcDHhB/Dio/mKhw== + dependencies: + "@types/js-yaml" "^3.12.1" + "@types/node" "^10.12.0" + "@types/request" "^2.47.1" + "@types/stream-buffers" "^3.0.3" + "@types/tar" "^4.0.3" + "@types/underscore" "^1.8.9" + "@types/ws" "^6.0.1" + byline "^5.0.0" + execa "1.0.0" + isomorphic-ws "^4.0.1" + js-yaml "^3.13.1" + jsonpath-plus "^0.19.0" + openid-client "^4.1.1" + request "^2.88.0" + rfc4648 "^1.3.0" + shelljs "^0.8.2" + stream-buffers "^3.0.2" + tar "^6.0.2" + tmp-promise "^3.0.2" + tslib "^1.9.3" + underscore "^1.9.1" + ws "^7.3.1" + "@kyleshockey/object-assign-deep@^0.4.2": version "0.4.2" resolved "https://registry.npmjs.org/@kyleshockey/object-assign-deep/-/object-assign-deep-0.4.2.tgz#84900f0eefc372798f4751b5262830b8208922ec" @@ -3490,7 +3518,7 @@ dependencies: "@types/node" ">= 8" -"@open-draft/until@^1.0.0", "@open-draft/until@^1.0.3": +"@open-draft/until@^1.0.3": version "1.0.3" resolved "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q== @@ -4638,6 +4666,11 @@ "@types/node" "*" "@types/responselike" "*" +"@types/caseless@*": + version "0.12.2" + resolved "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" + integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== + "@types/classnames@^2.2.9": version "2.2.10" resolved "https://registry.npmjs.org/@types/classnames/-/classnames-2.2.10.tgz#cc658ca319b6355399efc1f5b9e818f1a24bf999" @@ -4710,11 +4743,6 @@ dependencies: "@types/express" "*" -"@types/cookie@^0.3.3": - version "0.3.3" - resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803" - integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow== - "@types/cookie@^0.4.0": version "0.4.0" resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz#14f854c0f93d326e39da6e3b6f34f7d37513d108" @@ -5000,7 +5028,7 @@ resolved "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.6.tgz#f1a1cb35aff47bc5cfb05cb0c441ca91e914c26f" integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw== -"@types/js-yaml@^3.12.5": +"@types/js-yaml@^3.12.1", "@types/js-yaml@^3.12.5": version "3.12.5" resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.5.tgz#136d5e6a57a931e1cce6f9d8126aa98a9c92a6bb" integrity sha512-JCcp6J0GV66Y4ZMDAQCXot4xprYB+Zfd3meK9+INSJeVZwJmHAW30BBEEkPzXswMXuiyReUGOP3GxrADc9wPww== @@ -5144,6 +5172,11 @@ resolved "https://registry.npmjs.org/@types/node/-/node-10.17.28.tgz#0e36d718a29355ee51cec83b42d921299200f6d9" integrity sha512-dzjES1Egb4c1a89C7lKwQh8pwjYmlOAG9dW1pBgxEk57tMrLnssOfEthz8kdkNaBd7lIqQx7APm5+mZ619IiCQ== +"@types/node@^10.12.0": + version "10.17.35" + resolved "https://registry.npmjs.org/@types/node/-/node-10.17.35.tgz#58058f29b870e6ae57b20e4f6e928f02b7129f56" + integrity sha512-gXx7jAWpMddu0f7a+L+txMplp3FnHl53OhQIF9puXKq3hDGY/GjH+MF04oWnV/adPSCrbtHumDCFwzq2VhltWA== + "@types/node@^12.0.0": version "12.12.53" resolved "https://registry.npmjs.org/@types/node/-/node-12.12.53.tgz#be0d375933c3d15ef2380dafb3b0350ea7021129" @@ -5392,6 +5425,16 @@ resolved "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.28.tgz#6bda7db8653fa62643f5ee69e9f69c11a392e3a6" integrity sha1-a9p9uGU/piZD9e5p6facEaOS46Y= +"@types/request@^2.47.1": + version "2.48.5" + resolved "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz#019b8536b402069f6d11bee1b2c03e7f232937a0" + integrity sha512-/LO7xRVnL3DxJ1WkPGDQrp4VTV1reX9RkC85mJ+Qzykj2Bdw+mG15aAfDahc76HtknjzE16SX/Yddn6MxVbmGQ== + dependencies: + "@types/caseless" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + form-data "^2.5.0" + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -5477,6 +5520,13 @@ dependencies: "@types/node" "*" +"@types/stream-buffers@^3.0.3": + version "3.0.3" + resolved "https://registry.npmjs.org/@types/stream-buffers/-/stream-buffers-3.0.3.tgz#34e565bf64e3e4bdeee23fd4aa58d4636014a02b" + integrity sha512-NeFeX7YfFZDYsCfbuaOmFQ0OjSmHreKBpp7MQ4alWQBHeh2USLsj7qyMyn9t82kjqIX516CR/5SRHnARduRtbQ== + dependencies: + "@types/node" "*" + "@types/styled-jsx@^2.2.8": version "2.2.8" resolved "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234" @@ -5555,6 +5605,11 @@ dependencies: "@types/node" "*" +"@types/tough-cookie@*": + version "4.0.0" + resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz#fef1904e4668b6e5ecee60c52cc6a078ffa6697d" + integrity sha512-I99sngh224D0M7XgW1s120zxCt3VYQ3IQsuw3P3jbq5GG4yc79+ZjyKznyOGIQrflfylLgcfekeZW/vk0yng6A== + "@types/uglify-js@*": version "3.0.4" resolved "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082" @@ -5562,6 +5617,11 @@ dependencies: source-map "^0.6.1" +"@types/underscore@^1.8.9": + version "1.10.23" + resolved "https://registry.npmjs.org/@types/underscore/-/underscore-1.10.23.tgz#cc672e8864000d288e1e39c609fd9cab84391ff3" + integrity sha512-vX1NPekXhrLquFWskH2thcvFAha187F/lM6xYOoEMZWwJ/6alSk0/ttmGP/YRqcqtCv0TMbZjYAdZyHAEcuU4g== + "@types/uuid@^8.0.0": version "8.0.0" resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0" @@ -5623,6 +5683,13 @@ resolved "https://registry.npmjs.org/@types/whatwg-streams/-/whatwg-streams-0.0.7.tgz#28bfe73dc850562296367249c4b32a50db81e9d3" integrity sha512-6sDiSEP6DWcY2ZolsJ2s39ZmsoGQ7KVwBDI3sESQsEm9P2dHTcqnDIHRZFRNtLCzWp7hCFGqYbw5GyfpQnJ01A== +"@types/ws@^6.0.1": + version "6.0.4" + resolved "https://registry.npmjs.org/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1" + integrity sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg== + dependencies: + "@types/node" "*" + "@types/ws@^7.0.0": version "7.2.6" resolved "https://registry.npmjs.org/@types/ws/-/ws-7.2.6.tgz#516cbfb818310f87b43940460e065eb912a4178d" @@ -7161,7 +7228,7 @@ base64-js@^1.0.2, base64-js@^1.2.0: resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== -base64url@3.x.x: +base64url@3.x.x, base64url@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d" integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A== @@ -10635,6 +10702,19 @@ exec-sh@^0.3.2: resolved "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== +execa@1.0.0, execa@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + execa@3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz#c08ed4550ef65d858fac269ffc8572446f37eb89" @@ -10664,19 +10744,6 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" - integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== - dependencies: - cross-spawn "^6.0.0" - get-stream "^4.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^4.0.0, execa@^4.0.1: version "4.0.2" resolved "https://registry.npmjs.org/execa/-/execa-4.0.2.tgz#ad87fb7b2d9d564f70d2b62d511bee41d5cbb240" @@ -11342,7 +11409,7 @@ fork-ts-checker-webpack-plugin@^4.0.5, fork-ts-checker-webpack-plugin@^4.1.4: tapable "^1.0.0" worker-rpc "^0.1.0" -form-data@^2.3.1, form-data@^2.3.2: +form-data@^2.3.1, form-data@^2.3.2, form-data@^2.5.0: version "2.5.1" resolved "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== @@ -11987,6 +12054,23 @@ got@^11.5.2: p-cancelable "^2.0.0" responselike "^2.0.0" +got@^11.6.2: + version "11.7.0" + resolved "https://registry.npmjs.org/got/-/got-11.7.0.tgz#a386360305571a74548872e674932b4ef70d3b24" + integrity sha512-7en2XwH2MEqOsrK0xaKhbWibBoZqy+f1RSUoIeF1BLcnf+pyQdDsljWMfmOh+QKJwuvDIiKx38GtPh5wFdGGjg== + dependencies: + "@sindresorhus/is" "^3.1.1" + "@szmarczak/http-timer" "^4.0.5" + "@types/cacheable-request" "^6.0.1" + "@types/responselike" "^1.0.0" + cacheable-lookup "^5.0.3" + cacheable-request "^7.0.1" + decompress-response "^6.0.0" + http2-wrapper "^1.0.0-beta.5.2" + lowercase-keys "^2.0.0" + p-cancelable "^2.0.0" + responselike "^2.0.0" + got@^9.6.0: version "9.6.0" resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" @@ -12154,7 +12238,7 @@ graphql-upload@^8.0.2: http-errors "^1.7.3" object-path "^0.11.4" -graphql@15.3.0, graphql@^15.0.0, graphql@^15.3.0: +graphql@15.3.0, graphql@^15.3.0: version "15.3.0" resolved "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz#3ad2b0caab0d110e3be4a5a9b2aa281e362b5278" integrity sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w== @@ -12330,7 +12414,7 @@ he@^1.1.0, he@^1.2.0: resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -headers-utils@^1.1.9, headers-utils@^1.2.0: +headers-utils@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/headers-utils/-/headers-utils-1.2.0.tgz#5e10d1bc9d2bccf789547afca5b991a3167241e8" integrity sha512-4/BMXcWrJErw7JpM87gF8MNEXcIMLzepYZjNRv/P9ctgupl2Ywa3u1PgHtNhSRq84bHH9Ndlkdy7bSi+bZ9I9A== @@ -13609,6 +13693,11 @@ isomorphic-form-data@~2.0.0: dependencies: form-data "^2.3.2" +isomorphic-ws@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" + integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== + isstream@~0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -14115,6 +14204,13 @@ jose@^1.27.1: dependencies: "@panva/asn1.js" "^1.0.0" +jose@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/jose/-/jose-2.0.2.tgz#fb22385b80c658cc7a0cae05b7086c04c6be49f4" + integrity sha512-yD93lsiMA1go/qxSY/vXWBodmIZJIxeB7QhFi8z1yQ3KUwKENqI9UA8VCHlQ5h3x1zWuWZjoY87ByQzkQbIrQg== + dependencies: + "@panva/asn1.js" "^1.0.0" + js-cookie@^2.2.1: version "2.2.1" resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" @@ -14407,6 +14503,11 @@ jsonparse@^1.2.0: resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= +jsonpath-plus@^0.19.0: + version "0.19.0" + resolved "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz#b901e57607055933dc9a8bef0cc25160ee9dd64c" + integrity sha512-GSVwsrzW9LsA5lzsqe4CkuZ9wp+kxBb2GwNniaWzI2YFn5Ig42rSW8ZxVpWXaAfakXNrx5pgY5AbQq7kzX29kg== + jsonpointer@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9" @@ -15302,7 +15403,7 @@ make-dir@^3.0.0, make-dir@^3.0.2: dependencies: semver "^6.0.0" -make-error@1.x, make-error@^1.1.1: +make-error@1.x, make-error@^1.1.1, make-error@^1.3.6: version "1.3.6" resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== @@ -15939,22 +16040,6 @@ ms@^2.0.0, ms@^2.1.1: resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -msw@^0.19.5: - version "0.19.5" - resolved "https://registry.npmjs.org/msw/-/msw-0.19.5.tgz#7d2a1a852ccf1644d3db6735d69fff6777aac33f" - integrity sha512-J5eQ++gDVZoHPC8gVXtWcakLjgmPipvFj/sEnlRV/WViXuiq2CamSqO3Wbh6H8bAmj+k2vUWCfcVT1HjMdKB2Q== - dependencies: - "@open-draft/until" "^1.0.0" - "@types/cookie" "^0.3.3" - chalk "^4.0.0" - cookie "^0.4.1" - graphql "^15.0.0" - headers-utils "^1.1.9" - node-match-path "^0.4.2" - node-request-interceptor "^0.2.5" - statuses "^2.0.0" - yargs "^15.3.1" - msw@^0.20.5: version "0.20.5" resolved "https://registry.npmjs.org/msw/-/msw-0.20.5.tgz#b6141080c0d8b17c451d9ca36c28cc47b4ac487a" @@ -16131,7 +16216,7 @@ node-fetch@^1.0.1: encoding "^0.1.11" is-stream "^1.0.1" -node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0: +node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== @@ -16214,7 +16299,7 @@ node-libs-browser@^2.2.1: util "^0.11.0" vm-browserify "^1.0.1" -node-match-path@^0.4.2, node-match-path@^0.4.4: +node-match-path@^0.4.4: version "0.4.4" resolved "https://registry.npmjs.org/node-match-path/-/node-match-path-0.4.4.tgz#516a10926093c0cc6f237d020685b593b19baebb" integrity sha512-pBq9gp7TG0r0VXuy/oeZmQsjBSnYQo7G886Ly/B3azRwZuEtHCY155dzmfoKWcDPGgyfIGD8WKVC7h3+6y7yTg== @@ -16273,14 +16358,6 @@ node-releases@^1.1.52, node-releases@^1.1.58: resolved "https://registry.npmjs.org/node-releases/-/node-releases-1.1.60.tgz#6948bdfce8286f0b5d0e5a88e8384e954dfe7084" integrity sha512-gsO4vjEdQaTusZAEebUWp2a5d7dF5DYoIpDG7WySnk7BuZDW+GPpHXoXXuYawRBr/9t5q54tirPz79kFIWg4dA== -node-request-interceptor@^0.2.5: - version "0.2.6" - resolved "https://registry.npmjs.org/node-request-interceptor/-/node-request-interceptor-0.2.6.tgz#541278d7033bb6a8befb5dd793f83428cf6446a2" - integrity sha512-aJW1tPSM7nzuZFRe+C/KSz22GJO3CVFMxHHmMGX8Z+tjP7TCIVbzeckLFVfJG68BdVgrdOOP7Ejc57ag820eyA== - dependencies: - debug "^4.1.1" - headers-utils "^1.2.0" - node-request-interceptor@^0.3.5: version "0.3.5" resolved "https://registry.npmjs.org/node-request-interceptor/-/node-request-interceptor-0.3.5.tgz#4b26159617829c9a70643012c0fdc3ae4c78ae43" @@ -16529,6 +16606,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-hash@^2.0.1: + version "2.0.3" + resolved "https://registry.npmjs.org/object-hash/-/object-hash-2.0.3.tgz#d12db044e03cd2ca3d77c0570d87225b02e1e6ea" + integrity sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg== + object-inspect@^1.7.0: version "1.7.0" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" @@ -16644,6 +16726,11 @@ octokit-pagination-methods@^1.1.0: resolved "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== +oidc-token-hash@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.0.tgz#acdfb1f4310f58e64d5d74a4e8671a426986e888" + integrity sha512-8Yr4CZSv+Tn8ZkN3iN2i2w2G92mUKClp4z7EGUfdsERiYSbj7P4i/NHm72ft+aUdsiFx9UdIPSTwbyzQ6C4URg== + omggif@1.0.7: version "1.0.7" resolved "https://registry.npmjs.org/omggif/-/omggif-1.0.7.tgz#59d2eecb0263de84635b3feb887c0c9973f1e49d" @@ -16712,6 +16799,20 @@ opencollective-postinstall@^2.0.2: resolved "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" integrity sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw== +openid-client@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/openid-client/-/openid-client-4.1.1.tgz#3e8a25584c4292e9b9b03e60358f5549fb85197a" + integrity sha512-/qch3I3v8UtO0A7wVgyXJJjGX/knR8bv06DQpLuKQqLG5u4AHcgusGuVKPKAcneLZvHKbKovF2+3e2ngXyuudA== + dependencies: + base64url "^3.0.1" + got "^11.6.2" + jose "^2.0.2" + lru-cache "^6.0.0" + make-error "^1.3.6" + object-hash "^2.0.1" + oidc-token-hash "^5.0.0" + p-any "^3.0.0" + opn@^5.5.0: version "5.5.0" resolved "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" @@ -16817,6 +16918,14 @@ overlayscrollbars@^1.10.2: resolved "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-1.13.0.tgz#1edb436328133b94877b558f77966d5497ca36a7" integrity sha512-p8oHrMeRAKxXDMPI/EBNITj/zTVHKNnAnM59Im+xnoZUlV07FyTg46wom2286jJlXGGfcPFG/ba5NUiCwWNd4w== +p-any@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/p-any/-/p-any-3.0.0.tgz#79847aeed70b5d3a10ea625296c0c3d2e90a87b9" + integrity sha512-5rqbqfsRWNb0sukt0awwgJMlaep+8jV45S15SKKB34z4UuzjcofIfnriCBhWjZP2jbVtjt9yRl7buB6RlKsu9w== + dependencies: + p-cancelable "^2.0.0" + p-some "^5.0.0" + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -16963,6 +17072,14 @@ p-retry@^3.0.1: dependencies: retry "^0.12.0" +p-some@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/p-some/-/p-some-5.0.0.tgz#8b730c74b4fe5169d7264a240ad010b6ebc686a4" + integrity sha512-Js5XZxo6vHjB9NOYAzWDYAIyyiPvva0DWESAIWIK7uhSpGsyg5FwUPxipU/SOQx5x9EqhOh545d1jo6cVkitig== + dependencies: + aggregate-error "^3.0.0" + p-cancelable "^2.0.0" + p-timeout@^3.1.0: version "3.2.0" resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" @@ -19793,6 +19910,11 @@ reusify@^1.0.4: resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rfc4648@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/rfc4648/-/rfc4648-1.4.0.tgz#c75b2856ad2e2d588b6ddb985d556f1f7f2a2abd" + integrity sha512-3qIzGhHlMHA6PoT6+cdPKZ+ZqtxkIvg8DZGKA5z6PQ33/uuhoJ+Ws/D/J9rXW6gXodgH8QYlz2UCl+sdUDmNIg== + rgb-regex@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1" @@ -20332,7 +20454,7 @@ shell-quote@1.7.2: resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== -shelljs@^0.8.3: +shelljs@^0.8.2, shelljs@^0.8.3: version "0.8.4" resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== @@ -20896,6 +21018,11 @@ stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" +stream-buffers@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz#5249005a8d5c2d00b3a32e6e0a6ea209dc4f3521" + integrity sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ== + stream-combiner@~0.0.4: version "0.0.4" resolved "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" @@ -21760,6 +21887,13 @@ tinycolor2@^1.4.1: resolved "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" integrity sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g= +tmp-promise@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.2.tgz#6e933782abff8b00c3119d63589ca1fb9caaa62a" + integrity sha512-OyCLAKU1HzBjL6Ev3gxUeraJNlbNingmi8IrHHEsYH8LTmEuhvYfqvhn2F/je+mjf4N58UmZ96OMEy1JanSCpA== + dependencies: + tmp "^0.2.0" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -21767,6 +21901,13 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +tmp@^0.2.0: + version "0.2.1" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + tmp@~0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz#ee434a4e22543082e294ba6201dcc6eafefa2877" @@ -22219,6 +22360,11 @@ undefsafe@^2.0.2: dependencies: debug "^2.2.0" +underscore@^1.9.1: + version "1.11.0" + resolved "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz#dd7c23a195db34267186044649870ff1bab5929e" + integrity sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw== + unfetch@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/unfetch/-/unfetch-4.1.0.tgz#6ec2dd0de887e58a4dee83a050ded80ffc4137db" @@ -23272,6 +23418,11 @@ ws@^7.2.3: resolved "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd" integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w== +ws@^7.3.1: + version "7.3.1" + resolved "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" + integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== + x-is-string@^0.1.0: version "0.1.0" resolved "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz#474b50865af3a49a9c4657f05acd145458f77d82"